代码拉取完成,页面将自动刷新
const { resolve } = require('path');
const { loadImage } = require('canvas')
const { makePic, test } = require('./pic_font')
const { curl, readdir, copyFile, deleteFile, exists, mkdir, stat, writeFile, cal_media_length, call_ffmpeg, sleep } = require('./util')
const dir_parent_root = '/Users/flyprotoss/work/guoying/stock-video-news-script/temp/'
// const dir_parent_root = 'D:\\work\\stock-video-news-script\\temp';
const api_root = 'http://jfj8.bjyihuilian.com/v1/pic/'
const pic_width = 1280
const pic_height = 720
const media_frame = 15 // 视频帧率
const bengStartSecond = 0.2 // 每场景从第x秒开始蹦字
const bengPerSecond = 0.1 // 每x秒蹦1个字
const fileBgWhitePic60 = resolve(__dirname, 'static', 'bg60.png');
const fileBgWhitePic80 = resolve(__dirname, 'static', 'bg80.png');
const fileBgWhitePic100 = resolve(__dirname, 'static', 'bg100.png');
/**
* 计算场景所需信息
*/
async function getScenes(item, dir_root) {
const scenes = []
// 首幕
let pathAudio = resolve(dir_root, 'mp3s', `audio_start.mp3`);
let is_exists = await exists(pathAudio);
if (!is_exists) pathAudio = ''; // TODO 默认音效
scenes.push({ title: item.title, date: item.date, company: item.company, analyst: item.analyst, pathAudio, is_first: true });
for (let j=0; j<item.data.length; j++) {
// pathVideo/pathImg, pathChart, subtitle, summary, audio(subs)
let pathVideo = resolve(dir_root, 'pics', `${j+1}-1.mp4`);
is_exists = await exists(pathVideo);
if (!is_exists) pathVideo = '';
let pathImg = resolve(dir_root, 'pics', `${j+1}-1.jpg`);
is_exists = await exists(pathImg);
if (!is_exists) pathImg = '';
let pathChart = resolve(dir_root, 'pics', `${j+1}-1-chart.png`);
is_exists = await exists(pathChart);
if (!is_exists) pathChart = '';
let pathAudio = resolve(dir_root, 'mp3s', `1-${j+1}.mp3`);
is_exists = await exists(pathAudio);
if (!is_exists) pathAudio = '';
let pathSubAudios = [];
for (let k=1; ;k++) {
let pathSubAudio = resolve(dir_root, 'mp3s', `1-${j+1}-${k}.mp3`);
is_exists = await exists(pathSubAudio);
if (is_exists) pathSubAudios.push(pathSubAudio);
else break;
}
// 中间幕
scenes.push({
subtitle: item.data[j].audio_text.replace(/\^/g, '') || '',
summary: (item.data[j].summary || '').trim(),
pathVideo,
pathImg,
pathChart,
pathAudio,
pathSubAudios
})
}
// 尾幕
pathAudio = resolve(dir_root, 'mp3s', `audio_end.mp3`);
is_exists = await exists(pathAudio);
if (!is_exists) pathAudio = ''; // TODO 默认音效
scenes.push({pathAudio, is_last: true});
return scenes;
}
/**
* 视频拆帧
*/
async function videoToPics(pathVideo, destPath, total_pic_count) {
// ffmpeg -i "test.mp4" -r 15 -q:v 2 -f image2 pic/%3d.jpg
await call_ffmpeg(`-i "${pathVideo}" -r ${media_frame} -q:v 2 -f image2 ${destPath}/%3d.jpg`);
// 补足或删除多余图片
let existVideoPicCount = 0;
for(let k=1; k<=999; k++) {
const pic_path = resolve(destPath, (1000 + k + '').substr(1) + '.jpg');
const is_exist = await exists(pic_path);
if(is_exist) {
if(k > total_pic_count) {
// 删除多余的图片
await deleteFile(pic_path)
} else {
existVideoPicCount++;
}
} else {
if(k <= total_pic_count) {
// 添加不足的图片
const pic_source_path = resolve(destPath, (1000 + (k % existVideoPicCount + 1) + '').substr(1) + '.jpg')
await copyFile(pic_source_path, pic_path)
}
}
}
}
async function getSubtitleIndexs (pathSubAudios, subtitle, total_pic_count) {
if (!pathSubAudios || pathSubAudios.length < 1) return null;
const subtitle_list = subtitle.split('|').filter(o => o.trim() !== '').map(o => o.trim());
const subtitle_pic_indexs = [];
// 计算每句话图片数
for(let i=0; i<subtitle_list.length; i++){
const subtitle_seconds = await cal_media_length(pathSubAudios[i]);
const subtitle_pic_count = Math.round(subtitle_seconds * media_frame);
subtitle_pic_indexs.push(i===0? subtitle_pic_count : subtitle_pic_count + subtitle_pic_indexs[i-1]);
}
subtitle_pic_indexs[subtitle_pic_indexs.length-1] = total_pic_count;
return subtitle_pic_indexs.map((o,i) => ({ subtitle: subtitle_list[i], index: o}));
}
// let lastFrameInfo = null;
function getFrameInfo(data) {
const { currLv, totalLv, destPath, width, height, bgImg, imgChart, imgChartBg, subtitle, summary, beng, is_first, is_last, title, date, company, analyst } = data;
const retVal = { destPath, width, height, bgImg, imgChart, imgChartBg, subtitle, summary, beng };
if (is_first) {
return Object.assign(retVal, { title, date, company, analyst, is_first });
} else if (is_last) {
return Object.assign(retVal, { title, company: '谢谢观看', is_last });
} else {
// lastFrameInfo = retVal;
return retVal;
}
}
function compareFrameInfo (o1, o2) {
if(Object.keys(o1).length !== Object.keys(o2).length) return false;
for (let k of Object.keys(o1)) {
// 除了目标path之外一样
if (k === 'destPath') continue;
if (o1[k] !== o2[k]) return false;
}
return true;
}
(async () => {
// await test();
// await makePic(JSON.parse('{"destPath":"/Users/flyprotoss/work/guoying/node_video_script/test/002.jpg","width":1280,"height":720,"bgImg":"/Users/flyprotoss/work/guoying/node_video_script/test/0021.jpg","imgChart":null,"imgChartBg":{"onload":null,"onerror":null},"subtitle":"预计公司2020-22年的归母净利润为51.21/60.82/73.93亿元,对应的EPS为1.88/2.23/2.71元","summary":"预计公司2020-22年的归母净利润为51.21 / 60.82 / 73.93亿元,对应的EPS为1.88 / 2.23 / 2.71元","beng":"预计公司2020-22年的归母净利润为51.21 / 60.82 "}'))
// return;
while (true) {
console.log('.')
const json = await curl(`${api_root}get_news_scripts_by_status/6`);
const list = JSON.parse(json);
for(let i=0; i<list.length; i++){
const cal_start = new Date().getTime();
const item_json = await curl(`${api_root}get_news_script/${list[i].id}`)
const item = JSON.parse(JSON.parse(item_json).data)
const dir_root = resolve(dir_parent_root, item.id.toString())
// 构建场景
const scenes = await getScenes(item, dir_root);
// 构建关键帧
let imgChartBg = await loadImage(fileBgWhitePic80);
for (let j=0; j<scenes.length; j++) {
// if (j !== 11) continue; // TODO 测试移除
const scene = scenes[j];
const destPath = resolve(dir_root, j.toString());
await mkdir(destPath);
// 计算场景时长
const seconds = await cal_media_length(scene.pathAudio);
scene.seconds = seconds;
// 此幕需要图片的数量
const total_pic_count = Math.round(seconds * media_frame);
scene.total_pic_count = total_pic_count;
// 视频->拆帧
if (scene.pathVideo) {
await videoToPics(scene.pathVideo, destPath, total_pic_count);
}
// 图表
let imgChart = null;
if (scene.pathChart) {
imgChart = await loadImage(scene.pathChart);
}
let subtitleAndIndexs = await getSubtitleIndexs(scene.pathSubAudios, scene.subtitle, total_pic_count);
let tmpFrameInfo = {};
for (let v=1; v<=total_pic_count; v++) {
let destPicPath = resolve(destPath, `${(1000+v).toString().substr(1)}.jpg`);
let bgImg = (scene.pathVideo ? destPicPath : scene.pathImg);
let subtitle = scene.subtitle;
if (subtitleAndIndexs != null) {
// 此场景需要显示多条字幕
let index = 0;
while (index < subtitleAndIndexs.length) {
if (v<=subtitleAndIndexs[index].index)
break;
index++;
}
// console.log(subtitle, index, subtitleAndIndexs)
subtitle = subtitleAndIndexs[index].subtitle;
}
const is_video = scene.pathVideo != null && scene.pathVideo.length > 0;
// 蹦字:summary从1s开始,每0.5s蹦出一个
let beng = '';
if (scene.summary) {
const bengLen = Math.floor((v - bengStartSecond * media_frame)/Math.ceil(media_frame * bengPerSecond));
if (bengLen > 0) {
// 18 -> 1
// 24 -> 2
// 30 -> 3
beng = scene.summary.substr(0, bengLen);
}
}
const frameInfo = getFrameInfo({
currLv:v,
totalLv:total_pic_count,
destPath: destPicPath,
width:pic_width,
height:pic_height,
bgImg,
imgChart: (v <= media_frame || v >= total_pic_count - media_frame ? null : imgChart), imgChartBg,
subtitle,
beng,
summary: scene.summary,
is_first: scene.is_first || false,
is_last: scene.is_last || false,
title: item.title, date: item.date, company: item.company, analyst: item.analyst
});
// 合成帧
// title: item.title, date: item.date, company: item.company, analyst: item.analyst
if (!compareFrameInfo(frameInfo, tmpFrameInfo)) {
// 信息不相同,才合成帧,相同的直接复制->见下面补图代码
// if (frameInfo.subtitle && frameInfo.subtitle.indexOf('考虑到公司国内新能源整车龙头地位稳固')>-1)
// console.log(frameInfo)
await makePic(frameInfo);
tmpFrameInfo = frameInfo;
}
}
// 复制音频
await copyFile(scene.pathAudio, resolve(destPath, '0.mp3'));
// console.log(scene)
}
// 补图
for (let j=0; j<scenes.length; j++) {
const scene = scenes[j]
let lastExistFile = '';
for (let v=1; v<=scene.total_pic_count; v++) {
let destPicPath = resolve(resolve(dir_root, j.toString()), `${(1000 + v).toString().substr(1)}.jpg`);
let is_exist = await exists(destPicPath);
if (is_exist) lastExistFile = destPicPath;
else {
await copyFile(lastExistFile, destPicPath);
}
}
}
// 帧->合成视频
let files_txt_content = '';
for (let j=0; j<scenes.length; j++) {
// 图片合成视频 -> 0.mp4
await call_ffmpeg(`-r ${media_frame} -i ${resolve(dir_root, j.toString(), '%03d.jpg')} ${resolve(dir_root, j.toString(), '0.mp4')} -y`);
// 视频合成声音
const dest_video_file = resolve(dir_root, j.toString(), '00.mp4')
await deleteFile(dest_video_file)
// 合并声音 -> 00.mp4: ffmpeg -i 0.mp4 -i 0.mp3 00.mp4
await call_ffmpeg(`-i ${resolve(dir_root, j.toString(), '0.mp4')} -i ${resolve(dir_root, j.toString(), '0.mp3')} ${resolve(dir_root, j.toString(), '00.mp4')}`)
files_txt_content += `file '${dir_root}/${j}/00.mp4'\n`
// lv, currLv, totalLv, destPath, width, height,
// bgImg, imgChartBg, imgChart, subtitle, summary, first: {...}
}
// 组合成一个视频
const file = resolve(dir_root, 'files.txt')
await writeFile(file, files_txt_content)
// ffmpeg -f concat -safe 0 -i files.txt -c copy -y out-1.mp4
await deleteFile(resolve(dir_root, 'out.mp4'))
const _files = resolve(dir_root, 'files.txt')
const _out_720_mp4 = resolve(dir_root, 'out_720.mp4');
await call_ffmpeg(`-f concat -safe 0 -i ${_files} -c copy -y ${_out_720_mp4}`)
// 转成720p
// ffmpeg -i out0.mp4 -vb 500k -vcodec h264 -r 15 -s 1280x720 -ar 44100 -ac 2 -ab 96k -acodec aac out.mp4 -y
const _out_mp4 = _out_720_mp4.replace('out_720', 'out')
await call_ffmpeg(`-i ${_out_720_mp4} -vb 500k -vcodec h264 -r ${media_frame} -s 1280x720 -ar 44100 -ac 2 -ab 96k -acodec aac ${_out_mp4} -y`)
// 上传 转 python
// 更新每个场景的时长
const lengths = scenes.map(o => o.seconds.toString()).map(o => o.length > 5 ? o.substr(0, 5) : o).join('@')
await curl(`${api_root}edit_news_script_length/${item.id}/${lengths}`);
// 更新状态:待上传
await curl(`${api_root}edit_news_scripts_status/${item.id}/7`);
const cal_end = new Date().getTime();
console.log('finished...' + item.id + '...' + (cal_end - cal_start)/1000 + 's')
}
await sleep(5000)
}
})();
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。