【自動動画制作アプリを作る】Javascript、Node.js, GPT API
はじめに
こんにちは、最近、VSCodeでReactに簡単に触ったことにきっかけで、今日はJavascriptで興味ができ、何か実用的なアプリをつくってみたくなりました。何がいいか思って、動画を自動的につくってくれるアプリケーションを制作したら、みんな簡単に動画用意ができるようになると思います。
もちろん、最初から最後まで動画を作ることではなくて、最近、物凄く流行っている「GhatGPT」とのAPIを通じて、作成してみます。この記事をお読みになる読者の方々も、ぜひ、試してください!
実装過程
1.開発環境のキッチンインストール
青い背景の5秒の動画を作る。
.\ ffmpeg -f lavfi -i color=c=blue:s=1920x1080:r=30:d=5 -vf "fps=30" output.mp4
.\ffmpeg -f lavfi -i color=c=black:s=1920x1080:r=30:d=5 -vf "fps=30, drawtext=text='Hello World! Umaku Ikimasita~!':fontfile=
:fontsize=48:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2" output.mp4
2.テキストを音声へ
テキストを音声へ変換するのはTTS(Text to Speech)といいます。
下記は「google cloud tts」を使ってテキストを音声へ変換するコードです。
// Google Cloud TTS ライブラリを使用するために必要なパッケージを取得します。
const textToSpeech = require('@google-cloud/text-to-speech');
// Google Cloud TTSクライアントを作成します。
const client = new textToSpeech.TextToSpeechClient();
//TTSを生成するテキストを指定します。
const text = '.......';
// TTSリクエストに必要なパラメータを設定します。
const request = {
input: { text },
voice: { languageCode: 'ko-KR', ssmlGender: 'NEUTRAL' },
audioConfig: { audioEncoding: 'MP3' },
};
//TTSを生成する関数を定義します。
async function generateTTS() {
try {
// TTSリクエストを送信し、応答を受け取ります。
const [response] = await client.synthesizeSpeech(request);
//応答の中で、オーディオデータを抽出します。
const audioContent = response.audioContent;
// 抽出したオーディオ データをファイルとして保存します。
const fs = require('fs');
fs.writeFileSync('output.mp3', audioContent, 'binary');
console.log('TTS 生成が完了しました。');
} catch (error) {
console.error('TTS 作成中にエラーが発生しました:', error);
}
}
// TTS 生成関数を呼び出しています。
generateTTS();
上記のサイトにアクセスして、新しいプロジェクトを生成しましょう。
const textToSpeech = require('@google-cloud/text-to-speech');
const fs = require('fs');
//自分のキーファイルのパスを指定するコード
const client = new textToSpeech.TextToSpeechClient({
keyFilename: './aimovie-***.json'
});
...
generateTTS();
https://console.developers.google.com/apis/api/texttospeech.googleapis.com/overview?project=….
3.画像を入れる!:node-canvas、GPT API
「node-canvas」をインストールします。
https://www.npmjs.com/package/node-canvas
OpenAIのAPIを使ってみましょう!
$OPENAI_API_KEY = "YOUR_API_KEY"
Invoke-RestMethod -Uri "https://api.openai.com/v1/images/generations" `
-Method POST `
-Headers @{
"Content-Type" = "application/json"
"Authorization" = "Bearer $OPENAI_API_KEY"
} `
-Body '{
"prompt": "A cute baby sea otter",
"n": 2,
"size": "1024x1024"
}' | ConvertTo-Json
しかし、この行為を100度も繰り返ししなければならないなら、非常に面倒でしょう。コードで作ってみましょう。
import fetch from 'node-fetch';
for (let i = 0; i < 10; i++) {
fetch("https://api.openai.com/v1/images/generations", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authoriziation": "Bearer 自分のAPIキー"
},
body: JSON.stringify({
"prompt": "A cute baby sea otter",
"n": 1,
"size": "1024x1024"
})
})
.then(response => response.json())
.then(data => {
// Handle the response data here
console.log(data);
})
.catch(error => {
// Handle any errors here
console.error(error);
});
}
4.ChatCompletion(GPT)を利用した映像企画書作成自動化
Write a short article on the subject "~".
INSTRUCTION
- Wrtie in ~ language.
- Each sentences in the article must be short.
- Response in KJSON Array format like ["",...]and split it in 4 sentences.
テーマに対するイメージの生成向けたプロンプト作り-くれて動詞を備えた文章よりは、名詞形がいい。
不適切 A dog is walking along the beach.
不適切 There is a dog walking along the beach.
適切 A dog walking along the beach.
GPTに例を示して学習させる。
Write a DALL-E Prompt creates an image of "Summer vacation"
Promptes EXAMPLES
- An armchir in the shape of an avocade
- A photo of a silhouette of a person
- An abstract oil painting of a river
- A cartoon of a cat catching a mouse
- A cat riding a motorcycle
- A futuristic neon lit cyborg face
INSTRUCTION
- Response only on prompt.
- Response the Prompt for DALL-E without any explanations.
- DALL-E promptes should start with an article like A or An.
5.すべての機能を使用して、動画の完成させる(+Stability.AI)
Chat Completion
import { OpenAI } from 'openai';
const apiKey = 'MY API KEY~~!~!';
const openai = new OpenAI({ apiKey });
async function main() {
const completion = await openai.chat.completions.create({
messages: [{ role: 'user', content: 'Hello.' },
{ role: 'assistant', content: 'hi there~0' },
{ role: 'user', content: 'I am a Sonsan, what is your name?' }],
model: 'gpt-3.5-turbo',
});
console.log(completion.choices[0]);
}
main();
まず、自分のAPI Keyをにします。
「role」で「syste」・「user」・「assistant」役割をさせます。
async function main() {
const completion = await openai.chat.completions.create({
messages: [
{ role: 'system', content: 'assistant is a youtube script writer' },
{ role: 'user', content: `
Write a short article on the subject how to be a good programmer
INSTRUCTION
- Wrtie in japanese language.
- Each sentences in the article must be short.
- Response in KJSON Array format like ["",...]and split it in 6 sentences.' `}],
model: 'gpt-3.5-turbo',
temperature:1.0,
});
「main()」関数の中で、企画書を作成する、プロンプトを入力します。
*残念ながら、Dalleは無料サビスが終わって、無料クレジットを提供する「stability.ai」で試しました。
下記はいままで作成した、画像・テキスト変換・音声を総合したコードです。
//モジュールのインポート
import { promisify } from 'util';
import { exec } from 'child_process';
import textToSpeech from '@google-cloud/text-to-speech';
import fs from 'fs';
import { createCanvas, registerFont } from 'canvas';
import fetch from 'node-fetch';
import OpenAI from 'openai';
//キーの設定
let SDXLAPI = '…'; // https://platform.stability.ai/account/keys
let APIKey = '…'; // https://platform.openai.com/account/api-keys
const client = new textToSpeech.TextToSpeechClient({
keyFilename: './….json'
});
//テキストファイルのサイズ測定関数
function measureTextSize(text, fontPath, fontSize, maxWidth) {
registerFont(fontPath, { family: 'Custom Font' });
const canvas = createCanvas();
const context = canvas.getContext('2d');
context.font = `${fontSize}px 'Custom Font'`;
let words = text.split(' ');
let line = '';
let lines = [];
for (let i = 0; i < words.length; i++) {
let testLine = line + words[i] + ' ';
let metrics = context.measureText(testLine);
let testWidth = metrics.width;
if (testWidth > maxWidth && i > 0) {
lines.push(line);
line = words[i] + ' ';
} else {
line = testLine;
}
}
lines.push(line);
return lines.join('\n');
}
//Text-to-Speech関数
async function synthesizeSpeech(text, languageCode, ssmlGender, audioEncoding, fileName) {
const request = {
input: { text: text },
voice: { languageCode: languageCode, name: `${languageCode}-Wavenet-C`, ssmlGender: ssmlGender },
audioConfig: { audioEncoding: audioEncoding },
};
try {
const [response] = await client.synthesizeSpeech(request);
await fs.promises.writeFile(fileName, response.audioContent, 'binary');
return fileName;
} catch (err) {
console.error(err);
}
}
//音声ファイルの時間測定関数
const measureDurationOfAudioFile = promisify((audioFilePath, callback) => {
exec(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 ${audioFilePath}`, (error, stdout, stderr) => {
if (error) {
callback(error);
return;
}
const duration = parseFloat(stdout);
callback(null, duration);
});
});
const outputConcatCommand = (audioFiles, outputFileName) => {
let concatString = '';
for (let i = 0; i < audioFiles.length; i++) {
concatString += audioFiles[i].fileName;
if (i !== audioFiles.length - 1) {
concatString += '|';
}
}
const command = `ffmpeg -y -i "concat:${concatString}" -acodec copy ${outputFileName}`;
return command;
};
const runCommand = promisify((command, callback) => {
exec(command, (error, stdout, stderr) => {
if (error) {
callback(error);
return;
}
callback(null);
});
});
//スクリプトを音声に変換する関数
const scriptToVoice = async (script, languageCode) => {
const ssmlGender = 'NEUTRAL';
const audioEncoding = 'MP3';
let duration = 0;
let totalDuration = 0;
let audioFiles = [];
let startTime = 0;
for (let i = 0; i < script.length; i++) {
const fileName = `.output${i}.mp3`;
let audioFilePath = await synthesizeSpeech(script[i], languageCode, ssmlGender, audioEncoding, fileName);
if (!audioFilePath) continue;
try {
duration = await measureDurationOfAudioFile(audioFilePath);
} catch (error) {
console.error(error);
}
audioFiles.push({
"fileName": fileName,
"duration": duration,
"startTime": startTime,
"endTime": startTime + duration,
"script": script[i]
});
startTime += duration;
totalDuration += duration;
}
let destinationFile = '.voice.mp3';
const command = outputConcatCommand(audioFiles, destinationFile);
await runCommand(command);
for (let i = 0; i < script.length; i++) {
const fileName = `.output${i}.mp3`;
try {
await fs.promises.unlink(fileName);
} catch (err) {
}
}
return {
destinationFile,
audioFiles,
totalDuration
}
};
//スクリプトの実行
(async () => {
let keyword = process.argv[2];
let sentenceCount = process.argv[3] || 4;
let languageCode = process.argv[4] || 'ko-KR';
let translateCode = process.argv[5];
const openai = new OpenAI({ apiKey: APIKey });
async function translate(sentence) {
const completion = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [
{ role: "system", content: "assistant is a translator" },
{
role: "user", content: `
${sentence}
指示
- ${languageCode}の文を${translateCode}に翻訳してください
` },
],
});
const resultScript = completion.choices[0].message.content;
return resultScript.trim();
}
async function writeScript() {
const completion = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [
{ role: "system", content: "assistant is a youtube script writer" },
{
role: "user", content: `
"${keyword}"に関する短い記事を書いてください。
指示
- ${languageCode}で書いてください。
- 記事の各文は短くなければなりません。
- JSON配列形式で["", ...]のように応答し、${sentenceCount}文に分割してください。
` },
],
});
const resultScript = JSON.parse(completion.choices[0].message.content);
return resultScript;
}
async function dallePromptMaker() {
const completion = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [
{ role: "system", content: "assistant is a dall-e prompt engineer" },
{
role: "user", content: `
Write a DALL-E Prompt creates an image o${keyword}"
Prompts EXAMPLES
- An armchair in the shape of an avocado
- A photo of a silhouette of a person
- An abstract oil painting of a river
- A cartoon of a cat catching a mouse
- A cat riding a motorcycle
- A futuristic neon lit cyborg face
INSTRUCTION
- Response the Prompt for DALL-E without any explanations.
- DALL-E Prompts should start with an article like A or An for noun.
- All words in the prompt must be in alphabetical order.
- Response the prompt as only one sentence.さい。
` },
],
});
const resultScript = completion.choices[0].message.content;
return resultScript;
}
// スクリプトの準備
let script;
try {
script = await writeScript();
} catch (error) {
console.log("スクリプトの準備に失敗し、動画作成を中止します");
process.exit(1);
}
if (!Array.isArray(script) || script.length === 0 || !script.every(item => typeof item === 'string')) {
console.log("スクリプトの準備に失敗し、動画作成を中止します");
process.exit(1);
}
// スクリプトを読んで音声ファイルに変換
let result;
try {
result = await scriptToVoice(script, languageCode)
console.log('---------')
console.log('スクリプト生成')
console.log('---------')
result.audioFiles.map(data => data.script).forEach((line, no) => console.log('script', no, line))
} catch {
console.log("スクリプトを音声に変換する作業で失敗し、動画作成を中止します");
process.exit(1);
}
// スクリプトの翻訳
if (translateCode) {
console.log('-------')
console.log('スクリプト翻訳')
console.log('-------')
try {
for (let audioFile of result.audioFiles) {
const translated = await translate(audioFile.script)
console.log(audioFile.script + '\n' + translated)
audioFile.script = audioFile.script + '\n\n' + translated
}
} catch {
console.log("スクリプトの翻訳作業で失敗し、動画作成を中止します");
process.exit(1);
}
}
// 画像生成
try {
const prompt = await dallePromptMaker();
console.log('---------')
console.log('画像生成')
console.log('---------')
console.log('stable-diffusionプロンプト', '|', prompt)
if (false) {
// const response = await fetch("https://api.openai.com/v1/images/generations", {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// "Authorization": `Bearer ${APIKey}`
// },
// body: JSON.stringify({
// "prompt": prompt,
// "n": 1,
// "size": "512x512"
// })
// });
// const data = await response.json();
// const imageUrl = data?.data[0]?.url;
// if (imageUrl) {
// const imageResponse = await fetch(imageUrl);
// const arrayBuffer = await imageResponse.arrayBuffer();
// const buffer = Buffer.from(arrayBuffer);
// fs.writeFileSync('.img.png', buffer);
// }
} else {
const path = "https://api.stability.ai/v1/generation/stable-diffusion-xl-beta-v2-2-2/text-to-image";
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${SDXLAPI}`
};
const body = {
width: 512,
height: 512,
steps: 50,
seed: 0,
cfg_scale: 7,
samples: 1,
style_preset: "enhance",
text_prompts: [
{
"text": prompt,
"weight": 1
}
],
};
const response = await fetch(path, {
headers,
method: "POST",
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`Non-200 response: ${await response.text()}`)
const responseJSON = await response.json();
responseJSON.artifacts.forEach((image, index) => {
fs.writeFileSync('.img.png', Buffer.from(image.base64, 'base64'))
})
}
} catch (error) {
console.log("画像生成作業で失敗し、動画作成を中止します");
process.exit(1);
}
// スクリプトと音声ファイルを使用して動画を作成
const screenSize = { width: 1920, height: 1080 };
const fontPath = 'C:/Windows/Fonts/Arial.ttf'
const fontSize = 80
const maxWidth = screenSize.width - 100;
const totalDuration = result.totalDuration;
const currentDate = new Date();
const formattedDate = currentDate.toISOString().replace(/[-:.]/g, "");
const outputFileName = `${keyword} - ${formattedDate}.mp4`;
console.log(outputFileName)
let mpegCommand = `
ffmpeg -y -f lavfi -i "color=c=black:s=${screenSize.width}x${screenSize.height}:d=${totalDuration}" -i ${result.destinationFile} -i .img.png -filter_complex "
overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/4,
${(() => {
return result.audioFiles.map((scriptInfo, index) => {
const text = scriptInfo.script;
const startTime = scriptInfo.startTime;
const endTime = scriptInfo.endTime;
const wrappedText = measureTextSize(text, fontPath, fontSize, maxWidth);
const fileName = `.text${index}.txt`;
fs.writeFileSync(fileName, wrappedText);
return `drawtext=textfile='${fileName}':fontfile=${fontPath}:fontcolor=white:fontsize=${fontSize}:x=(w-text_w)/2:y=h-th-100:enable='between(t,${startTime},${endTime})'`
}).join(',');
})()}
" -pix_fmt yuv420p "${outputFileName}"
`;
mpegCommand = mpegCommand.split('\n').join('')
await runCommand(mpegCommand);
{
// 作成したリソースを削除するための動画
try { fs.unlinkSync('.img.png') } catch { }
try { fs.unlinkSync('.voice.mp3') } catch { }
for (let i = 0; ; i++) {
try {
fs.unlinkSync(`.text${i}.txt`)
} catch {
break;
}
}
}
console.log('完了')
})();
最後に
ChatGPTは最近、物凄く話題になったAIです。GPTさんが提供するAPIを利用して、いろいろアプリを開発すれば、すごい結果物が出るかもしれないです。この例として、JS、 Node.js、 FFmpeg、 Google Cloud TTS、 node-canvas、Dalle(代わりに、stability.ai)、OpenAI ChatGPTのAPIを総合的に利用してみました!!
今日は私もこのすぐいAIさんを通じて、自動化されることを探して、より生産性を向上させたいと思います!皆様もぜひ、挑戦してみてください!
エンジニアファーストの会社 株式会社CRE-CO
ソンさん
この記事が気に入ったらサポートをしてみませんか?