10
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Unity】PowerPointファイル表示+音声合成でスライドの動画作成を自動化

Posted at

先日PowerPointファイルを利用して発表動画を作成する機会があったのですが,何度も噛んだり,制限時間に間に合わなかったり撮り直すことになりました.

すごく喉が痛くなったので,機械に読み上げてもらうようにしようと思った次第です.

やろうとしていることは以下のようなものです.

構成図.png

  1. PowerPointファイルの読み込み
  2. スライドを画像に変換し,ノートに書かれている内容を取得
  3. 画像をUnityに読み込み
  4. ノートの文を音声合成APIに投げ,音声ファイルを作成
  5. スライド画像を表示し,音声を再生させる
  6. (アバターを表示し,口パクさせる)

今回は5までのやり方を示そうと思います.
ここまででナレーションありのスライド動画をUnityで表示させることができるようになります.ナレーションはスライドのメモを読み上げる形で実現します.

UnityRecorder等を利用することで動画をエクスポートすることもできます.

今回の記事で実装した内容はGitHubで公開しています.
詳しくはそちらを確認いただけるとありがたいです.

6まで実装すると以下のような動画がPowerPointファイルを投げるだけで作成できるようになりました.
sample.png
YouTubeの怪しい美容広告みたいになりました.

PowerPointファイルの読み込み

Microsoft.Office.Interop.PowerPointライブラリを利用することで,C#で.pptxや.pptといったPowerPointファイルを開いたり,修正,画像の書き出しといったことが可能になります.

以下のコードで,スライドを1枚の画像として書き出し,ノートをstringで取得します.

var SLIDE_PATH = "スライドの場所"
var FILE_PATH = "画像の保存場所"

// ノートと画像保存場所のリスト
var slideList = new List<(string, string)>();

var app = new Microsoft.Office.Interop.PowerPoint.Application();

// スライドを開く
var ppt = app.Presentations.Open(SLIDE_PATH, MsoTriState.msoTrue, MsoTriState.msoFalse,
    MsoTriState.msoFalse);

var width = (int) ppt.PageSetup.SlideWidth;
var height = (int) ppt.PageSetup.SlideHeight;

var slideList = new List<SlideDataRaw>();

for (var i = 1; i <= ppt.Slides.Count; i++)
{
    // 非表示スライドは無視
    if (ppt.Slides[i].SlideShowTransition.Hidden == MsoTriState.msoTrue) continue;

    // ノート
    var note = ppt.Slides[i].NotesPage.Shapes.Placeholders[2].TextFrame.TextRange.Text;
    if (note == "") continue;

    // JPEGとして保存
    var file = FILE_PATH + $"/slide{i:0000}.jpg";
    ppt.Slides[i].Export(file, "jpg", width, height);

    slideList.Add((note, file));
}

ppt.Close();
app.Quit();

UnityでPowerPointファイルを利用するのに一番問題となるのは,画像の保存場所です.
一般的にUnityではデータの保存場所としてApplication.persistentDataPathなどを利用しますが,これらは実行時Unityがアクセス権を持っており,PowerPointSDKからこの場所にデータの保存が行えないという問題がありました.
なのでFILE_PATHにはC:\Users\User\DocumentsなどUnityが触れない場所を設定する必要があります.

スライド画像をUnityに読み込む

一般的な.jpgファイルの読み込み方法と同じです.
以下のスクリプトで.jpgファイルをTexture2Dに変換します.

private static Texture2D LoadImage(string path)
{
    byte[] binary;
    try
    {
        using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
        {
            var length = (int) fs.Length;
            binary = new byte[length];
            fs.Read(binary, 0, length);
            fs.Close();
        }
    }
    catch(IOException exception)
    {
        Debug.Log(exception);
        return null;
    }
    
    var texture = new Texture2D(0, 0);
    texture.LoadImage(binary);
    return texture;
}

テキストを音声合成しAudioClipを作成

先ほど取得できたスライドのノートを音声合成APIに送信し,AudioClipを作成します.
音声合成は必要になった場合にリアルタイムで合成結果を受け取るストリーミング音声合成が一般的ですが,今回は発言内容が事前にすべて決まっているため,先にリクエストを全て送信しておき,結果をキャッシュするようにします.

今回は音声合成APIとしてWatson Text To Speech APIを利用しました.

Watson Text To Speechの利用方法

  1. IBM Cloudにログイン(アカウントを持っていない場合は作成します)

  2. カタログからText To Speechを選択します.
    image.png

  3. リージョンを選択し,「作成」を押します.
    image.png

  4. リソースリストから今作成したサービスを選択し,APIキーとURLをメモしておきます
    image.png

これでWatson側の設定は以上です.
WatsonにはIBM Watson SDK for Unityという便利なUnity用SDKが存在し,これを利用することで多くのサンプルやUtilityスクリプトを利用できますが,今回はText To Speechのみを利用するため,自分でWebRequestを実装します.


まず,先ほど取得したAPIキーからアクセストークンを取得するリクエストを記述する必要があります.音声合成リクエストにはAPIキーではなくこのトークンを送る必要があります.

トークンを取得するリクエストは以下のように記述します.

public string GetAccessToken(string apikey)
{
    var form = new WWWForm();
    form.AddField("grant_type", "urn:ibm:params:oauth:grant-type:apikey");
    form.AddField("apikey", apiKey);
    form.AddField("response_type", "cloud_iam");
    using (var request = UnityWebRequest.Post(AUTH_URL, form))
    {
        request.SetRequestHeader("Content-type", "application/x-www-form-urlencoded");
        request.SendWebRequest();
        while(!request.isDone && !_cancelled){}

        if (request.responseCode != 200L)
        {
            Debug.LogError($"[GenerateAudio] Request Failed ({request.responseCode}): {request.error}\nat{request.url}");
            return;
        }

        var json = request.downloadHandler.text;
        return JsonConvert.DeserializeObject<IamTokenResponse>(json).AccessToken;
    }
}

今回はJsonのパースにNewtonsoft Jsonを利用しました.どんなものでもよく,AccessTokenパラメータのValueが目的のアクセストークンです.

一応,Newtonsoft Jsonの場合のパースする対象オブジェクトは以下のような構造になっています.

public class IamTokenResponse
{
    [JsonProperty("access_token", NullValueHandling = NullValueHandling.Ignore)]
    public string AccessToken { get; set; }
    [JsonProperty("refresh_token", NullValueHandling = NullValueHandling.Ignore)]
    public string RefreshToken { get; set; }
    [JsonProperty("token_type", NullValueHandling = NullValueHandling.Ignore)]
    public string TokenType { get; set; }
    [JsonProperty("expires_in", NullValueHandling = NullValueHandling.Ignore)]
    public long? ExpiresIn { get; set; }
    [JsonProperty("expiration", NullValueHandling = NullValueHandling.Ignore)]
    public long? Expiration { get; set; }
}

そしてリクエストを送信し,合成結果を取得するのは以下のように記述します.

public byte[] GetSynthesizeVoice(string text)
{
    var rqStr = JsonConvert.SerializeObject(new JObject {["text"] = text});
    var url = $"{URL}/v1/synthesize?voice=ja-JP_EmiV3Voice";
    using (var request = new UnityWebRequest(url, "POST"))
    {
        request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(rqStr));
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "application/json");
        request.SetRequestHeader("Accept", "audio/wav");
        request.SetRequestHeader("Authorization", $"Bearer {ACCESS_TOKEN}");
        request.SendWebRequest();
        while(!request.isDone && !_cancelled){}
        
        if (request.responseCode != 200L)
            return null;
        return request.downloadHandler.data;
    }
}

ACCESS_TOKENには先ほど取得したアクセストークンを,URLにはWatsonのページで取得したUrlを記述してください.


これで合成音声のバイナリを取得できたので,UnityのAudioClipに変換する作業を行います.方法は私の前回の記事Google Cloud Text-To-Speechを利用してUnityでキャラクターをフルボイスに!でも使わせていただいた,WAVクラスを利用します.

var wav = new WAV(GetSynthesizeVoice("こんにちは"));
var audioClip = AudioClip.Create("TextToSpeech", wav.SampleCount, 1, wav.Frequency, false);

スライドを表示

最後に,画像表示と音声再生を同時に行い,音声が終了したら次のスライドを表示し音声も再生させる機構を実装します.

実装は簡単で,音声再生に使うAudioSourceをUpdateで監視しておき,isPlaying(再生中)がfalseになったら次の処理を行います.

public class Presenter : MonoBehaviour
{
    [SerializeField] private RawImage image;
    [SerializeField] private AudioSource source;
    
    private int _currentIndex;
    private SlideData[] _slide;

    public void StartPresentation(SlideData[] data)
    {
        Debug.Log("[Presenter] Start presentation.");
        _slide = data;
        _currentIndex = -1;
    }

    private void Update()
    {
        if (_slide == null || source.isPlaying) return;
        if (_currentIndex >= _slide.Length - 1) return;
        Debug.Log("[Presenter] Next slide.");
        ++_currentIndex;
        image.texture = _slide[_currentIndex].image;
        source.clip = _slide[_currentIndex].clip;
        source.Play();
    }
}

これでスライドの読み込みから音声合成,スライド送りが実装できました.

終わりに

今回はPowerPointファイルをUnityで読み込み,音声合成を行うことでナレーションありのスライド動画を自動で作成する手順を紹介しました.

今回示したコードのサンプルは,実際には「リクエスト可能サイズに合わせたテキストの分割」や「分割された音声ファイルの結合」が必要です.そのような処理も含めた,UniSlideToMovieというサンプルプロジェクトをGitHubで公開していますのでそちらも確認していただけたらと思います.

また,この次の記事として,今回触れられなかった「6. アバターを表示し,口パクさせる」処理について書きたいと思います.そちらもよろしくお願いします.

10
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?