「〇〇で歌ってみた」動画のパワポ素材を生成する【python-pptx】

#概要
「〇〇で歌ってみた」動画の素材として用いることを想定し、本来の歌詞、替え歌歌詞、対応する画像を決まった場所に配置した画像をPowerPointのスライドとして生成するpythonコードを書きました。
<img width="1280" alt="スクリーンショット 2020-03-22 18.50.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/5e09c20b-2472-423d-2a03-4177f645518f.png">
#背景
特定ジャンルの名詞だけで本来の歌詞を再現するように歌った替え歌を、通称「〇〇で歌ってみた」シリーズとよびます。
「〇〇で歌ってみた」動画では、本来の歌詞、替え歌歌詞、対応する画像を決まった場所に配置した下記のような画像を、紙芝居形式で流す事が多いです。
<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/0c64b6ee-0084-b738-4ebb-80092462153a.png">
替え歌歌詞に登場する名詞数が多いときは対応する動画素材を手動で作るのが大変なので、プログラムで自動化したいと思いました。
またプログラムで生成したものを人が微調整できるようしておけば更に便利だと思ったので、PowerPointのスライドとして、作ることにしました。
このために、[python-pptx](https://python-pptx.readthedocs.io/en/latest/index.html)というpythonからpptxファイルを生成できるライブラリを使いました。
#環境
macOS Catalina バージョン10.15.3
python3.8.0またはpython3.7.0
Microsoft PowerPoint for Mac バージョン16.40(20081000)
#用意するもの
##画像ファイル
パワーポイントに挿入する画像を用意します。
今回は[こちらの方法](https://qiita.com/shimajiroxyz/items/bebcdb06b47c6b399063)で入手した国旗画像を使います。
##名詞と画像ファイル名の対応表
名詞IDと対応する画像ファイル名をカンマ区切りで下記のように記入したcsvファイルを用意します([こちらの方法](https://qiita.com/shimajiroxyz/items/bebcdb06b47c6b399063)のおまけなどを参照してください)。名詞IDは後述の歌詞定義ファイル中で使用します。

```:name_to_img.csv
アイスランド,Flag_of_Iceland.png
アイルランド,Flag_of_Ireland.png
アゼルバイジャン,Flag_of_Azerbaijan.png
アフガニスタン,Flag_of_Afghanistan.png
アブハジア,Flag_of_the_Republic_of_Abkhazia.png
アメリカ合衆国,Flag_of_the_United_States.png
アラブ首長国連邦,Flag_of_the_United_Arab_Emirates.png
アルジェリア,Flag_of_Algeria.png
...
```
##歌詞定義ファイル
本来の歌詞、替え歌歌詞、表示する画像の名詞IDをカンマ区切りで1行に1つずつ記入した下記のようなcsvを用意します。
名詞IDの部分はファイル名をいちいち書くのが手間だと感じたので、名詞IDをファイル名に置き換える対応表を別途用意する形式にしました。
画像ファイル名を直接書くのでも構わない場合は、対応表なしでも構いません。

```:lyric.csv
風の中のスバル,カザフスタン ツバル,カザフスタン
風の中のスバル,カザフスタン ツバル,ツバル
砂の中の銀河,スワジランド ギニア,スワジランド
砂の中の銀河,スワジランド ギニア,ギニア
みんなどこへ行った,インドネシア インド,インドネシア
みんなどこへ行った,インドネシア インド,インド
見守られることもなく,マリ モナコ レソト モナコ,マリ共和国
見守られることもなく,マリ モナコ レソト モナコ,モナコ
見守られることもなく,マリ モナコ レソト モナコ,レソト
見守られることもなく,マリ モナコ レソト モナコ,モナコ
...
```

##ファイル構成
画像ファイルを格納するフォルダ名をpictures、名詞と画像ファイル名の対応表をname_to_img.csv、歌詞定義ファイルをlyric.csvとします。
またパワポを生成するpythonスクリプトをgenerate_pptx.pyとします。
これらのファイルを下記のように配置してください。

```
.
├── generate_pptx.py
├── pitcures
│   ├── Flag_of_Afghanistan.png
│   ├── Flag_of_Albania.png
│   └── ...
├── name_to_img.csv
├── lyric.csv
```
#コード
前述のファイル構成において、「〇〇で歌ってみた」動画の素材となるパワーポイントファイルを出力するpythonスクリプトについて説明します。コード全体は末尾に載せておきます。本コードを基本として編集することで、文字や画像の追加、配置の変更などもできます。必要に応じて[python-pptxの公式ドキュメント](https://python-pptx.readthedocs.io/en/latest/index.html)も参照してください。
##ライブラリのインストール
python-pptxとPillowを使いますので、必要に応じて下記コマンド等によりインストールしてください。

```
pip install python-pptx
pip install Pillow
```
##importと各種定数の設定
ライブラリのimportと各種定数の設定をします。
スライドサイズは単位がEnglish Metric Unit (EMU; 1 cm = 360,000 emu) であることに注意してください。
（参考：[python-pptxでスライドのサイズを変えたい](https://ja.stackoverflow.com/questions/37638/python-pptx%E3%81%A7%E3%82%B9%E3%83%A9%E3%82%A4%E3%83%89%E3%81%AE%E3%82%B5%E3%82%A4%E3%82%BA%E3%82%92%E5%A4%89%E3%81%88%E3%81%9F%E3%81%84)）

```Python
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN
from PIL import Image
from pptx.dml.color import RGBColor

#スライドサイズ
#4:3 (default) 9144000x6858000
#16:9 12193200x6858000
SLIDE_WIDTH = 12193200
SLIDE_HEIGHT = 6858000
BACKGROUD_RGB = RGBColor(0,0,0)
FONT_RGB = RGBColor(255,255,255)

OUTPUT_FILE_PATH = "test.pptx"

#替え歌歌詞テキストボックスの左上座標の高さ (左右方向は中央揃えするので不要)
IMG_FILE_PATH = "name_to_img.csv"#1行につきidと画像パスがひとつずつ書かれたcsvファイル
IMG_DIR = "./picture/"
LYRIC_FILE_PATH = "lyric.csv"#1行につき本来の歌詞、替え歌歌詞、画像ファイルIDが一つずつ書かれたcsvファイル

PARODY_LYRIC_HEIGHT = Inches(5.5)
PARODY_LYRIC_FONTSIZE = Pt(36)

ORIGINAL_LYRIC_HEIGHT = Inches(6.5)
ORIGINAL_LYRIC_FONTSIZE = Pt(28)

IMG_DISPLAY_HEIGHT = Inches(3) #スライドに表示するときの画像の高さ。とりあえず3インチとしておく。
IMG_CENTER_X, IMG_CENTER_Y = SLIDE_WIDTH/2, SLIDE_HEIGHT/2 #画像の中心座標```
```
##設定ファイルの情報を取得
対応表の情報をname2path（dict型）、歌詞定義ファイルの情報をlyrics（list型）という変数に代入します。

```Python
name2path = {}
with open(IMG_FILE_PATH,"r") as f:
    text = [v.split(',') for v in f.read().split("\n")]
    for v in text:
        if len(v) == 2:
            name2path[v[0]]=IMG_DIR+v[1]
lyrics=[]
with open(LYRIC_FILE_PATH,"r") as f:
    lyrics = [v.split(',') for v in f.read().split("\n") if len(v.split(','))==3]
```
##プレゼンテーションオブジェクトの生成とサイズ指定
スライドサイズはPresentation.slide_width、Presentation.slide_heightに値を代入することで変更できます。

```Python
#スライドオブジェクトの定義
prs = Presentation()
#スライドサイズの指定
prs.slide_width = SLIDE_WIDTH
prs.slide_height = SLIDE_HEIGHT
```
##for文でlyricsの各要素に対応するスライドを追加する
ここからfor文でlyricsの各要素に対応するスライドを追加していく処理について説明します。まずlyricの0、1、2番めの要素がそれぞれ本来の歌詞（original_text）、替え歌歌詞（parody_text）、画像ID（img_id）に対応しているので、取得します。
画像IDが対応表に存在しない場合は、continueします。存在する場合は、ファイルパス（img_file）を取得します。

```Python
for index,lyric in enumerate(lyrics):
    original_text = lyric[0]
    parody_text = lyric[1]
    img_id = lyric[2]
    if img_id not in name2path:
        print("img_id",img_id,"does not exist") 
        print("line",index,":",lyric,"is ignored") 
        continue 
    img_file = name2path[img_id] 
```

##白紙スライドを追加する
白紙のスライドをPresentationオブジェクトにadd_slideします。

```Python
    #白紙スライドの追加      
    blank_slide_layout = prs.slide_layouts[6]    
    slide = prs.slides.add_slide(blank_slide_layout) 
```
##スライドの背景を黒くする

```Python
    #背景を黒くする
    shapes = slide.shapes
    left, top, width, height = 0, 0, SLIDE_WIDTH, SLIDE_HEIGHT
    shape = shapes.add_textbox(left,top,width,height)
    fill = shape.fill
    fill.solid()
    fill.fore_color.rgb = BACKGROUD_RGB
```
スライドと同じサイズのtextBoxを追加し、黒く塗りつぶします。
##画像の挿入
画像をIMG_CENTER_X, IMG_CENTER_Yを中心とする位置に挿入します。
本来、python-pptxによるadd_pictureでは左上の座標位置しか指定できませんが、「〇〇で歌ってみた」動画では絵が切り替わる前後で画像中心が揃ってたほうが見栄えがいいので、画像サイズを別途取得した上で、センタリングで挿入します。
詳しいやり方は[python-pptxで画像をセンタリングする](https://qiita.com/shimajiroxyz/items/f57efa222765ac9eea39)にまとめたので参考にしてください。

```Python
    #画像サイズを取得してアスペクト比を得る
    im = Image.open(img_file)
    im_width, im_height = im.size
    aspect_ratio = im_width/im_height

    #表示された画像のサイズを計算
    img_display_height = IMG_DISPLAY_HEIGHT
    img_display_width = aspect_ratio*img_display_height

    #センタリングする場合の画像の左上座標を計算
    left = IMG_CENTER_X - img_display_width / 2
    top = IMG_CENTER_Y - img_display_height / 2

    #画像をスライドに追加
    slide.shapes.add_picture(img_file, left, top, height = IMG_DISPLAY_HEIGHT)
```
##替え歌歌詞の挿入
替え歌歌詞を追加します。
テキストボックスの幅をスライド幅と同じに指定した上で、PP_ALIGN.CENTERで左右方向のセンタリングをすることで、歌詞が真ん中に表示されるようにします。
文字色、文字サイズはそれぞれfont.size、font.color.rgbに値を代入して指定します。

```Python
    #テキストボックスを追加 
    left, top, width, height = 0, PARODY_LYRIC_HEIGHT, SLIDE_WIDTH, Inches(1) 
    txBox = slide.shapes.add_textbox(left, top, width, height) 
    tf = txBox.text_frame 
    tf.text = parody_text
    paragraph = tf.paragraphs[0]
    font = paragraph.font 
    font.size = PARODY_LYRIC_FONTSIZE
    font.color.rgb = FONT_RGB 
    paragraph.alignment = PP_ALIGN.CENTER #左右方向のセンタリング
```
##本来の歌詞の挿入
本来の歌詞を挿入します。やり方は替え歌歌詞の挿入と同じです。

```Python
    #テキストボックスを追加 
    left, top, width, height = 0, ORIGINAL_LYRIC_HEIGHT, SLIDE_WIDTH, Inches(1) 
    txBox = slide.shapes.add_textbox(left, top, width, height) 
    tf = txBox.text_frame 
    tf.text = original_text
    paragraph = tf.paragraphs[0]
    font = paragraph.font 
    font.size = ORIGINAL_LYRIC_FONTSIZE 
    font.color.rgb = FONT_RGB
    paragraph.alignment = PP_ALIGN.CENTER 
```
##出力
for文を終えたら、Presentation.saveでpptxファイルを出力します。

```Python
prs.save(OUTPUT_FILE_PATH)
```
#コード全体
コード全体は下記です。

```Python:generate_pptx.py

from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN
from PIL import Image
from pptx.dml.color import RGBColor

#スライドサイズ
#4:3 (default) 9144000x6858000
#16:9 12193200x6858000
SLIDE_WIDTH = 12193200
SLIDE_HEIGHT = 6858000
BACKGROUD_RGB = RGBColor(0,0,0)
FONT_RGB = RGBColor(255,255,255)

OUTPUT_FILE_PATH = "test.pptx"

#替え歌歌詞テキストボックスの左上座標の高さ (左右方向は中央揃えするので不要)
IMG_FILE_PATH = "name_to_img.csv"#1行につきidと画像パスがひとつずつ書かれたcsvファイル
IMG_DIR = "./picture/"
LYRIC_FILE_PATH = "lyric.csv"#1行につき本来の歌詞、替え歌歌詞、画像ファイルIDが一つずつ書かれたcsvファイル

PARODY_LYRIC_HEIGHT = Inches(5.5)
PARODY_LYRIC_FONTSIZE = Pt(36)

ORIGINAL_LYRIC_HEIGHT = Inches(6.5)
ORIGINAL_LYRIC_FONTSIZE = Pt(28)

IMG_DISPLAY_HEIGHT = Inches(3) #スライドに表示するときの画像の高さ。とりあえず3インチとしておく。
IMG_CENTER_X, IMG_CENTER_Y = SLIDE_WIDTH/2, SLIDE_HEIGHT/2 #画像の中心座標

name2path = {}
with open(IMG_FILE_PATH,"r") as f:
    text = [v.split(',') for v in f.read().split("\n")]
    for v in text:
        if len(v) == 2:
            name2path[v[0]]=IMG_DIR+v[1]
lyrics=[]
with open(LYRIC_FILE_PATH,"r") as f:
    lyrics = [v.split(',') for v in f.read().split("\n") if len(v.split(','))==3]

#スライドオブジェクトの定義
prs = Presentation()
#スライドサイズの指定
prs.slide_width = SLIDE_WIDTH
prs.slide_height = SLIDE_HEIGHT

for index,lyric in enumerate(lyrics):
    original_text = lyric[0]
    parody_text = lyric[1]
    img_id = lyric[2]
    if img_id not in name2path:
        print("img_id",img_id,"does not exist") 
        print("line",index,":",lyric,"is ignored") 
        continue 
    img_file = name2path[img_id] 

    #白紙スライドの追加      
    blank_slide_layout = prs.slide_layouts[6]    
    slide = prs.slides.add_slide(blank_slide_layout) 
    
    #背景を黒くする
    shapes = slide.shapes
    left, top, width, height = 0, 0, SLIDE_WIDTH, SLIDE_HEIGHT
    shape = shapes.add_textbox(left,top,width,height)
    fill = shape.fill
    fill.solid()
    fill.fore_color.rgb = BACKGROUD_RGB
    
    #画像サイズを取得してアスペクト比を得る
    im = Image.open(img_file)
    im_width, im_height = im.size
    aspect_ratio = im_width/im_height
    
    #表示された画像のサイズを計算
    img_display_height = IMG_DISPLAY_HEIGHT
    img_display_width = aspect_ratio*img_display_height
    
    #センタリングする場合の画像の左上座標を計算
    left = IMG_CENTER_X - img_display_width / 2
    top = IMG_CENTER_Y - img_display_height / 2
    
    #画像をスライドに追加
    slide.shapes.add_picture(img_file, left, top, height = IMG_DISPLAY_HEIGHT)
      
    #テキストボックスを追加 
    left, top, width, height = 0, PARODY_LYRIC_HEIGHT, SLIDE_WIDTH, Inches(1) 
    txBox = slide.shapes.add_textbox(left, top, width, height) 
    tf = txBox.text_frame 
    tf.text = parody_text
    paragraph = tf.paragraphs[0]
    font = paragraph.font 
    font.size = PARODY_LYRIC_FONTSIZE
    font.color.rgb = FONT_RGB 
    paragraph.alignment = PP_ALIGN.CENTER 
    
    #テキストボックスを追加 
    left, top, width, height = 0, ORIGINAL_LYRIC_HEIGHT, SLIDE_WIDTH, Inches(1) 
    txBox = slide.shapes.add_textbox(left, top, width, height) 
    tf = txBox.text_frame 
    tf.text = original_text
    paragraph = tf.paragraphs[0]
    font = paragraph.font 
    font.size = ORIGINAL_LYRIC_FONTSIZE 
    font.color.rgb = FONT_RGB
    paragraph.alignment = PP_ALIGN.CENTER 

prs.save(OUTPUT_FILE_PATH)
```

#おわりに
pptxファイルが出力されたら、微調整をして、書き出し機能でpngなどに書き出すことで、動画編集ソフトの素材として使えると思います。
上記のコードでできあがるのはシンプルなスライドですが、歌詞や画像の挿入位置、文字色や背景色を変えるなどして、好みのデザインにしてみてください。