Skip to content

AWS の What's New(英語版)を自動翻訳する

AWS では、What's New として最新発表ページが用意されていますが、公式での日本語翻訳は数日遅れるため、最新を追うためには、英語のページを見ておくことが最良だと思っています。

思っているのですが、英語へのハードルが高いので、RSS から当日公開されているページを拾い、Amazon Translate を使って自動翻訳した翻訳文と原文を一つの HTML ページに仕上げ、静的ウェブホスティングをしている S3 バケットへアップロードする仕組みを作ってみました。

ついでに、似非 Podcast 的に利用できればいいなと思い、翻訳文を Amazon Polly を利用して簡易的な読み上げファイルを作成し、翻訳ページ内で再生できるようにしています。

出来上がったページ

AWS 最新情報(自動翻訳版)

トップページ

auto-translate-whatsnew-aws-02

通知の個別ページ

auto-translate-whatsnew-aws-03

当初、テーブル部分が長くなる懸念があったが、jQuery の DataTables プラグインを利用した検索とソート、ページネーションを導入して解決。

処理の概要

auto-translate-whatsnew-aws

実装内容

以前のブログ でもやっていたように、内部の処理は bash で作成しています。

今回は、シェルスクリプトの中で AWS CLI を利用しているので、AWS CLI を利用可能な Lambda レイヤーの作成を、以下のサイトを参考に作成しました。

参考にさせていただいたサイト

Lambda レイヤーにライブラリを追加する ( AWS CLI 編) | Grasswake‘s Blog

Lambda レイヤーの作成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 各種作業ディレクトリの作成
mkdir ~/aws_cli_layer
cd ~/aws_cli_layer
mkdir aws-cli aws-bin bin

# AWS CLI のインストールファイルのダウンロード
wget https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip

# ファイルの解凍
unzip awscli-exe-linux-x86_64.zip

# 作業用ディレクトリへのインストール
./aws/install -i ~/aws_cli_layer/aws-cli -b ~/aws_cli_layer/aws-bin

# aws コマンドのバイナリーファイルなどが入ったディレクトリの中身を全てコピー
cp -r ./aws-cli/v2/current/dist/* ./bin

# 必要なファイルを格納した bin ディレクトリを zip ファイル化
cd ~/aws_cli_layer
zip -r9 aws_cli_layer.zip bin

# 上記までで作成した aws_cli_layer.zip から Lambdaレイヤーを作成
aws lambda publish-layer-version --layer-name aws_cli_layer \
  --zip-file fileb://aws_cli_layer.zip
  --compatible-runtimes provided.al2

通常、上記の aws lambda publish-layer-version サブコマンドの実行で、Lambda レイヤーの作成ができるが、zip ファイルの容量が大きく、エラー発生したため、以下の方法で、S3 バケットから読み込ませる方法で対応しました。

1
2
3
aws lambda publish-layer-version --layer-name aws_cli_layer \
  --content S3Bucket=${S3_BUCKET_NAME},S3Key=aws_cli_layer.zip \
  --compatible-runtimes provided.al2

bootstrap の作成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh

set -euo pipefail

# Initialization - load function handler
source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh"

# Processing
while true; do
  HEADERS="$(mktemp)"
  # Get an event
  EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

  # Execute the handler function from the script
  RESPONSE=$($(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA")

  # Send the response
  curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response" -d "$RESPONSE"
done

処理プログラムの作成(functions.sh)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
function handler() {
  EVENT_DATA=$1
  echo "$EVENT_DATA" 1>&2
  RESPONSE=$(#!/bin/sh

cd /tmp
curl https://aws.amazon.com/about-aws/whats-new/recent/feed/ > tmp.xml
xmllint --format tmp.xml > whatsnew.xml
NUM=$(cat whatsnew.xml | grep "<item>" | wc -l)

aws s3 cp s3://${S3_BUCKET_NAME}/index.html .
mkdir -p audio

export LANG=C
DATE=$(date +'%a, %d %b %Y' --date '1 day ago')
FILE_DATE=$(date +'%Y%m%d' --date '1 day ago')

cat << EOF > template.html
<html lang="ja">
        <meta charset="utf-8">
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
        <link href="./css/common.css" rel="stylesheet">
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
        <script src='https://code.jquery.com/jquery-3.4.1.min.js'></script>
        <script src="./js/common.js"></script>
    <title>TITLE</title>
    <body>
        <header><h1><a href="./index.html">AWS 最新情報(自動翻訳版)</a></h1></header>
        <div class="container">
        EN
        <h3>JP_TIT</h3>
        <div class="jp-text">
EOF

for ((i=1; i < ${NUM}; i++)); do
  XMLDATE=$(xmllint --xpath "//item[${i}]/pubDate" whatsnew.xml | sed 's/<[^>]*>//g')

  if [[ "${XMLDATE}" =~ "${DATE}" ]]; then
    URL=$(xmllint --xpath "//item[${i}]/link" whatsnew.xml | sed 's/<[^>]*>//g')
    FILENAME=$(echo $URL | awk -F '/' '{print $(NF-1)}')
    curl ${URL} > ${FILENAME}-tmp.html

    EN=$(cat ${FILENAME}-tmp.html | \
      tr '\r\n' '\t' | \
      sed -e 's/\t//g' | \
      grep -oP '<main id="aws-page-content-main" role="main" tabindex="-1">.*?</main>')

    UP_DATE=$(cat ${FILENAME}-tmp.html | \
      tr '\r\n' '\t' | \
      sed -e 's/\t//g' | \
      grep -oP '<span class="date">.*?</span>' \
      | sed 's/<[^>]*>//g')

    TITLE=$(cat ${FILENAME}-tmp.html | \
      tr '\r\n' '\t' | \
      sed -e 's/\t//g' | \
      grep -oP '<h1 id=".*?"> <a name=".*?">.*?</a> </h1>' \
      | sed 's/<[^>]*>//g')

    JP_WITHOUT_TAG=$(cat ${FILENAME}-tmp.html | \
      tr '\r\n' '\t' | \
      sed -e 's/\t//g' | \
      grep -oP '<main id="aws-page-content-main" role="main" tabindex="-1">.*?</main>' | \
      grep -oP '<div class="aws-text-box">.*?</div>' \
      | sed 's/<[^>]*>//g')

    JP=$(aws translate translate-text \
      --source-language-code "en" \
      --target-language-code "ja" \
      --text "${JP_WITHOUT_TAG}" \
      --query 'TranslatedText' \
      --output text)

    JP_TITLE=$(aws translate translate-text \
      --source-language-code "en" \
      --target-language-code "ja" \
      --text "${TITLE}" \
      --query 'TranslatedText' \
      --output text)

    cat template.html | \
      sed -e "s|EN|${EN}|g" | \
      sed -e "s|TITLE|${TITLE}|g"> ${FILE_DATE}-${FILENAME}.html

    echo ${JP}"</div>" >> ${FILE_DATE}-${FILENAME}.html

    JP_WITHOUT_TAG_FOR_POLLY=$(cat ${FILE_DATE}-${FILENAME}.html | \
      tr '\r\n' '\t' | \
      sed -e 's/\t//g' | \
      grep -oP '<div class="jp-text">.*?</div>' \
      | sed 's/<[^>]*>//g')

    aws polly synthesize-speech \
      --text "${JP_WITHOUT_TAG_FOR_POLLY}" \
      --voice-id Mizuki \
      --output-format mp3 ./audio/${FILE_DATE}-${FILENAME}.mp3

    AUDIO=$(printf '<audio controls="controls"><source type="audio/mpeg" src="./audio/'%s'"><a href="./audio/'%s'">./audio/'%s'</a></audio></div><footer>Created by <a href="https://twitter.com/kazzpapa3" target="_blank">@kazzpapa3</a></footer></body></html>' "${FILE_DATE}-${FILENAME}.mp3" "${FILE_DATE}-${FILENAME}.mp3" "${FILE_DATE}-${FILENAME}.mp3")
    echo ${AUDIO} >> ${FILE_DATE}-${FILENAME}.html

    LINK=$(printf '<tr><td>%s</td><td><a href="./'%s'">%s<br>%s</a></td><td><audio controls="controls"><source type="audio/mpeg" src="./audio/'%s'"><a href="./audio/'%s'">./audio/'%s'</a></audio></td></tr>' "${UP_DATE}" "${FILE_DATE}-${FILENAME}.html" "${TITLE}" "${JP_TITLE}" "${FILE_DATE}-${FILENAME}.mp3" "${FILE_DATE}-${FILENAME}.mp3" "${FILE_DATE}-${FILENAME}.mp3")

    sed -i "s|JP_TIT|${JP_TITLE}|g" ${FILE_DATE}-${FILENAME}.html
    sed -i "22a ${LINK}" index.html

    aws s3 cp ./audio/${FILE_DATE}-${FILENAME}.mp3 s3://${S3_BUCKET_NAME}/audio/
    aws s3 cp ${FILE_DATE}-${FILENAME}.html s3://${S3_BUCKET_NAME}/
    aws s3 cp index.html s3://${S3_BUCKET_NAME}/
  else
    break
  fi  
done

)

echo $RESPONSE
}

あとは、必要な権限のある IAM ロール、Lambda 関数を作成し、トリガーとして EventBridge の Cron 式で、毎日午前 8時(日本時間)に実行するようにしています。

今後やりたいこと

  • 音声ファイル、および翻訳文ページを紹介するツイートの自動化(かつ、なるべくなら bash でやりたい。)
  • このブログと同様に、CloudFront + S3 バケットでの静的ウェブホスティングに移行して HTTPS 化したいが、mp3 ファイルなどのキャッシュ具合など、CloudFront での課金発生がどの程度になるか読めずちょっと怖いので、一旦保留。

Comments