見出し画像

【自動動画制作アプリを作る】Javascript、Node.js, GPT API


はじめに


こんにちは、最近、VSCodeでReactに簡単に触ったことにきっかけで、今日はJavascriptで興味ができ、何か実用的なアプリをつくってみたくなりました。何がいいか思って、動画を自動的につくってくれるアプリケーションを制作したら、みんな簡単に動画用意ができるようになると思います。

もちろん、最初から最後まで動画を作ることではなくて、最近、物凄く流行っている「GhatGPT」とのAPIを通じて、作成してみます。この記事をお読みになる読者の方々も、ぜひ、試してください!


実装過程



1.開発環境のキッチンインストール

https://code.visualstudio.com/
アクセスして、インストールします。
https://nodejs.org/ja/download
\アクセスして、インストールします。
Windows build by BtbNをクリック
ダウンロードして、圧縮を解凍
Windows Power Shellでbinフォルダに入る


青い背景の5秒の動画を作る。

.\ ffmpeg -f lavfi -i color=c=blue:s=1920x1080:r=30:d=5 -vf "fps=30" output.mp4
Windows Power Shellに入力
output.mp4が生成された。
5秒の動画生成を確認
下記のコマンドを入力
  .\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
「Hello World! Umaku ikimasita~!」メッセージが表示された!


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();

「tts.js」ファイルを「C:\Users\…\Downloads\ttsproject」に生成します。


npmで「@google-cloud/text-to-speech」をインストールします。
npm i @google-cloud/text-to-speech
インストール完了
「node tts.js」で起動する。
でも、エラーに怒られた。
メッセージを読んだ、まだ、Google Cloudの設定が用意されなかった。

上記のサイトにアクセスして、新しいプロジェクトを生成しましょう。

プロジェクト名を入力
「APIとサービス」ー「ユーザー認証情報」をクリック
「サービスアカウントの管理」をクリック
「サービスアカウントの作成」をクリック
「サービスアカウント名」を入力後、「完了」ボタンをクリック
仕事クリック
キー管理クリック
新しいキーの作成クリック
キータイプを選択し、作るボタンをクリック
「aimovie-…..json」キーファイルをダウンロードして、
自分のフォルダに入れます。



const textToSpeech = require('@google-cloud/text-to-speech');
const fs = require('fs');

//自分のキーファイルのパスを指定するコード
const client = new textToSpeech.TextToSpeechClient({
    keyFilename: './aimovie-***.json'
});

... 


generateTTS();
「node tts.js」で起動した。
でも、TTSのAPIが設定されなった。
下記のサイトにアクセスし、使用をクリック

https://console.developers.google.com/apis/api/texttospeech.googleapis.com/overview?project=….

世の中、ただは珍しいよね…
この画面が表示されると、OK!
また、「node tts.js」を実行すると、
「voice.mp3」という音声ファイルが生成されます。


3.画像を入れる!:node-canvas、GPT API

「node-canvas」をインストールします。

https://www.npmjs.com/package/node-canvas

「npm i node-canvas」を入力します。


OpenAIのAPIを使ってみましょう!

メイン画面
「Create image」で「curl」でリクエストの形をコーピ
curl  
ブラウザですれば、こうなります。
Authrizationの内に「API Key」を入力します。
「API Key」をコーピ
「Bearer」の後で貼り付ける。
$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 
リスポンをVSCodeに貼り付けます。
「ALT+Z」を押して、まとめる。
リンクをクリックすると、
「PublicAccessNotPermitted」エラーに怒られた。
\u0026 → & (ampersand)
「\u0026」 をクリックし、
「CTRL+H」で、全文切り替えます。
リンクをクリックし、
かわいい動物が出てきました

しかし、この行為を100度も繰り返ししなければならないなら、非常に面倒でしょう。コードで作ってみましょう。

NodeFetchのインストール
まず、dalleというフォルダを作り、そこの経路に移動します。
次にnpm init-yes6を入力してインストールします。
npm i node-fetchも入力してインストールします。
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

「Create chat completion」クリック
「gpt/chatgpt.js」ファイル生成
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」役割をさせます。

まず、「npm install openai」インストール。
「node chatgpt.js」を実行すると、
リスポンの中にGPTさんの返答が入ってくる。
「temperature」を変更して、
返答の多様性を操る。
ここでは、「0」で何度も聞いてみても、
同じ返答だ。
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()」関数の中で、企画書を作成する、プロンプトを入力します。

「node chatgpt.js」実行。
「プログラマーになるための秘訣はなんでしょうか?」という質問について、返答がすごい。
「\n」の改行文字がはいっている。

*残念ながら、Dalleは無料サビスが終わって、無料クレジットを提供する「stability.ai」で試しました。

Keyを生成しましょう。

下記はいままで作成した、画像・テキスト変換・音声を総合したコードです。

//モジュールのインポート
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('完了')
})();
「node ファイル名、入力を受けるタイトル、文章数、言語」
動画がうまくいきますね。
サイズも調節
動画がうまくできました。
Youtubeでアプロード
うまくいきますね~


最後に


ChatGPTは最近、物凄く話題になったAIです。GPTさんが提供するAPIを利用して、いろいろアプリを開発すれば、すごい結果物が出るかもしれないです。この例として、JS、 Node.js、 FFmpeg、 Google Cloud TTS、 node-canvas、Dalle(代わりに、stability.ai)、OpenAI ChatGPTのAPIを総合的に利用してみました!!

今日は私もこのすぐいAIさんを通じて、自動化されることを探して、より生産性を向上させたいと思います!皆様もぜひ、挑戦してみてください!


エンジニアファーストの会社 株式会社CRE-CO
ソンさん




この記事が気に入ったらサポートをしてみませんか?