AWSを使った「なろう」をPollyで朗読させる

niwanos.hatenablog.com

前回は「なろう」WebAPIを作りましたが、今回はAWSの読み上げサービスPollyで 取得したチャプターを読み上げた音声ファイルをS3に保存してみます。

aws.amazon.com

const axios = require('axios')
const cheerio = require('cheerio')
const SocksProxyAgent = require('socks-proxy-agent');
const bookBaseUrl = 'https://ncode.syosetu.com/';
const proxyHost = '*******';
const proxyPort = '****';
const proxyOptions = `socks5://${proxyHost}:${proxyPort}`;
const httpsAgent = new SocksProxyAgent(proxyOptions);
const AWS = require("aws-sdk");
AWS.config.update({
  region: '*'
});
const S3 = new AWS.S3();
const Polly = new AWS.Polly();

let response;
/**
 *
 * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
 * @param {Object} event - API Gateway Lambda Proxy Input Format
 *
 * Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html
 * @param {Object} context
 *
 * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
 * @returns {Object} object - API Gateway Lambda Proxy Output Format
 *
 */
exports.lambdaHandler = async (event, context) => {
    try {
        let bookId = event.pathParameters.bookId;
        let chapterId = event.pathParameters.chapterId;
        let chapter = await exports.getChapter(bookId, chapterId);

        let text = chapter.title + '\n\n' + chapter.contents.join('\n');

        const pollyParams = {
          OutputFormat: 'mp3',
          Text: text,
          VoiceId: 'Mizuki',
          TextType: 'text'
        };

        let data = await Polly.synthesizeSpeech(pollyParams).promise();

        var s3Params = {
          Bucket: '******', 
          Key: `${bookChapterId}.mp3`,
          Body: new Buffer(data.AudioStream)
        };

        // s3にputする
        let s3Resp = await S3.putObject(s3Params).promise();

        response = {
            'statusCode': 200,
            'body': JSON.stringify({
                'message': "ok"
            }),
            'headers': {
                'Access-Control-Allow-Origin': '*'
            }
        }
    } catch (err) {
        console.log(err);
        return err;
    }

    return response
};

exports.getChapter = async (bookId, chapterId)=> {
  const url = exports.makeChapterUrl(bookId, chapterId);
  let resp = await axios.get(url, {httpsAgent: httpsAgent});
  const $ = cheerio.load(resp.data);
  let title = $('p.novel_subtitle').text();
  let contents = [];

  for(let i = 1; ; i++ ){
    if ($(`#L${i}`).length == 0){
      break;
    }
    contents.push($(`#L${i}`).text());
  }
  return {
    title: title,
    contents: contents
  };
}

exports.makeChapterUrl = (bookId, chapterId)=> {
  return `${bookBaseUrl}${bookId}/${chapterId}/`;
}

結果

https://d2daztqdssi217.cloudfront.net/n7975cr_1.mp3

ラノベを読み上げる音声、なかなか斬新

参考文献 www.yamamanx.com

qiita.com

AWSを使った「なろう」WebAPIの作成

「蜘蛛ですが何か?」が面白いと妻から聞いたので「なろう」のWebAPIをAWSで作ってみました。

構成

CloudFront=>APIGateway=>Lambda=>SocksProxy=>なろうサイト

  1. CloudFront
    • キャッシュサーバ SocksProxyを経由したために遅くなったレスポンスの改善&なろうサイトのサーバに負荷をかけないため設置
    • キャッシュに載せるには MethodをGetにする必要あり
  2. APIGateway
    • HttpRequest受け口
    • path paramter を取得してeventへセットしてくれるよう設定
  3. Lambda
    • リクエスト処理&html取得&Parse&整形&Response返却
    • nodejsで作成
    • httpリクエストはaxiosを使用
    • axiosでSocksProxyを使うためsocks-proxy-agentを使用
    • html parseはcheeroを使用(JQueryのようにSelectorでElementの取得が可能)
    • ResponseHeaderにAccess-Control-Allow-Originを追加することでcors制限を回避
  4. SocksProxy
    • なろうサイトがAWSIPアドレス帯からの接続を拒否しているための迂回
    • 今回はPIA VPNにくっ付いていたSocksProxyを使用
  5. なろうサイト
    • タイトルと本文を取得

リクエストサンプル

path parameterに本のIDと章を渡すとJSONでtitleとcontents(行単位のarray)が返却されます。

下記は蜘蛛ですが何かのの一つ目がJSONで返却されるサンプル(そのうち止めます)

curl https://d1oy3e79o7znce.cloudfront.net/fastread/n7975cr/1

上記URLに対応するなろうサイトのURLはこちら。

https://ncode.syosetu.com/n7975cr/1/

Lambdaのコードはこんな感じ

const axios = require('axios')
const cheerio = require('cheerio')
const SocksProxyAgent = require('socks-proxy-agent');
const bookBaseUrl = 'https://ncode.syosetu.com/';
const proxyHost = '**********';
const proxyPort = '****';
const proxyOptions = `socks5://${proxyHost}:${proxyPort}`;
const httpsAgent = new SocksProxyAgent(proxyOptions);

let response;

exports.lambdaHandler = async (event, context) => {
    try {
        let bookId = event.pathParameters.bookId;
        let chapterId = event.pathParameters.chapterId;
        let chapter = await exports.getChapter(bookId, chapterId);
        response = {
            'statusCode': 200,
            'body': JSON.stringify({
                title: chapter.title,
                contents: chapter.contents
            }),
            'headers': {
                'Access-Control-Allow-Origin': '*'
            }
        }
    } catch (err) {
        console.log(err);
        return err;
    }

    return response
};

exports.getChapter = async (bookId, chapterId)=> {
  const url = exports.makeChapterUrl(bookId, chapterId);
  let resp = await axios.get(url, {httpsAgent: httpsAgent});
  const $ = cheerio.load(resp.data);
  let title = $('p.novel_subtitle').text();
  let contents = [];

  for(let i = 1; ; i++ ){
    if ($(`#L${i}`).length == 0){
      break;
    }
    contents.push($(`#L${i}`).text());
  }
  return {
    title: title,
    contents: contents
  };
}

exports.makeChapterUrl = (bookId, chapterId)=> {
  return `${bookBaseUrl}${bookId}/${chapterId}/`;
}

template.yamlのpath paramter設定部分

          Properties:
            Path: /fastread/{bookId}/{chapterId}

参考文献 qiita.com

github.com

github.com

www.privateinternetaccess.com

github.com

javascriptでobjectのpathを列挙するサンプル

javascriptでObjectを全部舐めてpathを全て列挙する必要があったのでサンプルを作成してみました。

パス列挙関数

/**
 * オブジェクトを再帰で探索し全ての終端までのパスをドット区切りで列挙
 * @param {object} obj 探索対象Object
 * @param {string} path パス
 */
var getPaths = function(obj, path){
    // Objectでなければ終端とみなす
    if(!(obj instanceof Object)){
         return [path];
    }
    // パス列挙用Array
    var paths = []
    // オブジェクトLoop
    Object.keys(obj).forEach(function(key){
        // 直接設定したプロパティでなければ対象外
        if(!obj.hasOwnProperty(key)){
            return;
        }
        // 返却されたパス列挙Arrayを結合
        paths = paths.concat(getPaths(obj[key], path ? path + "." + key : key));
    });
    // パス列挙Arrayを返却
    return paths;
};

参考

obj instanceof Object

developer.mozilla.org 下記とてもわかりやすいBlog、今回はArrayとObjectを探索したいので instanceof ObjectでOK

  1. typeofとinstanceofについてまとめ tweeeety.hateblo.jp

obj.hasOwnProperty(key)

developer.mozilla.org

paths.concat

developer.mozilla.org

path ? path + "." + key : key

developer.mozilla.org

動作サンプル

これ

var A = function(data) {
    this.data = {};
    if(data) {
    this.data = data;
  }
};

var B = function(data) {
    this.data = {};
    if(data) {
    this.data = data;
  }
};

var a1 = new A({Aa: 1, Ab: 2, Ac: 3});
var b1 = new B({Ba: 4, Bb: 5, Bc: 6});
var a2 = new A({Aa: 7, Ab: 8, Ac: 9});
var b2 = new B({Ba: 10, Bb: 11, Bc: 12});

var obj = {
    p1: a1,
  p2: 10,
  p3: {
    p31: a2,
    p32: 20,
  },
  p4: {
    p5: {
        p6: b2
    }
  },
  p5: [
    {p51: 1},
    {p52: 2},
  ],
  p6: "test",
};

console.log(getPaths(obj));

のように実行すると

[
     "p1.data.Aa"
    ,"p1.data.Ab"
    ,"p1.data.Ac"
    ,"p2"
    ,"p3.p31.data.Aa"
    ,"p3.p31.data.Ab"
    ,"p3.p31.data.Ac"
    ,"p3.p32"
    ,"p4.p5.p6.data.Ba"
    ,"p4.p5.p6.data.Bb"
    ,"p4.p5.p6.data.Bc"
    ,"p5.0.p51"
    ,"p5.1.p52"
    ,"p6"
]

となります。

JSFiddleの実行サンプル jsfiddle.net

with句の再帰問合せを利用した抜け番検索

Oracle WITH句で再帰問合せができると聞いたので
抜け番検索を作ってみました。
http://www.oracle.com/technetwork/jp/articles/otnj-sql-image7-1525406-ja.html#c

まず抜け番のあるテーブルレコードを作成。

create table test(num number);
insert into test values(1);
insert into test values(3);
insert into test values(5);
insert into test values(7);
insert into test values(9);
insert into test values(10);
commit;

抜け番問合せ

with num_list(cnt) as (
	select 1 from dual
	union all
	select 1 + cnt from num_list where cnt < 10
)
select
	n.cnt
from
	n_list n
where
	not exitst(
		select
			1
		from
			test t
		where
			t.num = n.cnt)
)
order by n.cnt;

with句のエイリアスnum_listをwith句内で呼び出しています。
再帰問合せはUNION ALLのみ対応してるみたいですね。
union allの下のselectでwhere句にて最大件数を指定しないとサイクルしていると怒られます。

これに 12c で追加された fetch句で
fetch first 1 rows only
とかを記載すれば、空き番の一番若い番号を取得できそうですね。

蛇足ですが
抜け番を使用したinsertを行う場合は整合性を保つためにはテーブル毎ロックが必要です。


数年ぶりのブログなのでちょっと緊張

SQLLoaderのCTLファイル作成Function

tsv用

CREATE OR REPLACE FUNCTION MAKE_CTL
(
	P_TABLE_NAME IN VARCHAR2
)
--RETURN CLOB
RETURN VARCHAR2
IS
	CURSOR CUR_TABLE_COLUMN(V_TABLE_NAME VARCHAR2) IS SELECT COLUMN_NAME FROM USER_TAB_COLUMNS WHERE TABLE_NAME = V_TABLE_NAME;
--	CTL_STR CLOB;
	CTL_STR VARCHAR2(4000);
	CR CONSTANT CHAR(1) := CHR(13);
	LF CONSTANT CHAR(1) := CHR(10);
	NL CONSTANT CHAR(2) := CR || LF;
BEGIN
	FOR COL IN CUR_TABLE_COLUMN(P_TABLE_NAME) LOOP
		IF CUR_TABLE_COLUMN%ROWCOUNT > 1 THEN
			CTL_STR := CTL_STR || ',';
		ELSE
			CTL_STR := CTL_STR || ' ';
		END IF;
		CTL_STR := CTL_STR || COL.COLUMN_NAME || NL;
	END LOOP;

	CTL_STR :=
		         'OPTIONS(SKIP=1,ERRORS=-1)'
		|| NL || 'LOAD DATA'
		|| NL || 'REPLACE'
		|| NL || 'INTO TABLE ' || P_TABLE_NAME
		|| NL || 'FIELD TERMINATED BY X''09'''
		|| NL || 'TRAILING NULLCOLS'
		|| NL || '('
		|| NL || CTL_STR
		|| NL || ')';

	RETURN CTL_STR;
END;
/

TYPEのMEMBER FUNCTION PROCEDURE を使ってVARCHAR2をラッピングしてみた

sqlloader用のctlファイル作成function
tsv用

CREATE OR REPLACE FUNCTION MAKE_CTL
(
	P_TABLE_NAME IN VARCHAR2
)
--RETURN CLOB
RETURN VARCHAR2
IS
	CURSOR CUR_TABLE_COLUMN(V_TABLE_NAME VARCHAR2) IS SELECT COLUMN_NAME FROM USER_TAB_COLUMNS WHERE TABLE_NAME = V_TABLE_NAME;
--	CTL_STR CLOB;
	CTL_STR VARCHAR2(4000);
	CR CONSTANT CHAR(1) := CHR(13);
	LF CONSTANT CHAR(1) := CHR(10);
	NL CONSTANT CHAR(2) := CR || LF;
BEGIN
	FOR COL IN CUR_TABLE_COLUMN(P_TABLE_NAME) LOOP
		IF CUR_TABLE_COLUMN%ROWCOUNT > 1 THEN
			CTL_STR := CTL_STR || ',';
		ELSE
			CTL_STR := CTL_STR || ' ';
		END IF;
		CTL_STR := CTL_STR || COL.COLUMN_NAME || NL;
	END LOOP;

	CTL_STR :=
		         'OPTIONS(SKIP=1,ERRORS=-1)'
		|| NL || 'LOAD DATA'
		|| NL || 'REPLACE'
		|| NL || 'INTO TABLE ' || P_TABLE_NAME
		|| NL || 'FIELD TERMINATED BY X''09'''
		|| NL || 'TRAILING NULLCOLS'
		|| NL || '('
		|| NL || CTL_STR
		|| NL || ')';

	RETURN CTL_STR;
END;
/

TYPEのMEMBER FUNCTION PROCEDURE を使ってVARCHAR2をラッピングしてみた

CREATE OR REPLACE TYPE STRING_TYPE
AS OBJECT (
	 STR VARCHAR2(4000)
	,MEMBER FUNCTION LEN RETURN NUMBER
	,MEMBER FUNCTION SUBSTRING(S IN NUMBER,L IN NUMBER) RETURN VARCHAR2
	,MEMBER PROCEDURE PRINT
)
/
CREATE OR REPLACE TYPE BODY STRING_TYPE AS
	MEMBER FUNCTION LEN RETURN NUMBER
	IS
	BEGIN
		RETURN LENGTH(STR);
	END;
	MEMBER FUNCTION SUBSTRING(S IN NUMBER,L IN NUMBER) RETURN VARCHAR2
	IS
	BEGIN
		RETURN SUBSTR(STR,S,L);
	END;
	MEMBER PROCEDURE PRINT
	IS
	BEGIN
		DBMS_OUTPUT.PUT_LINE(STR);
	END;
END;
/

STRING_TYPEってtypeを作ってlengthとsubstrを実装した。
実装

CREATE OR REPLACE PROCEDURE TEST
IS
	MOJI STRING_TYPE;
BEGIN
	MOJI := STRING_TYPE('AIUEO');
	MOJI.PRINT;
	DBMS_OUTPUT.PUT_LINE('LENGTH=' || MOJI.LEN);
	DBMS_OUTPUT.PUT_LINE('SUBSTR(2,2)=' || MOJI.SUBSTRING(1,1));
END;
/

実行結果

SQL> SET SERVEROUTPUT ON
SQL> DECLARE
  2    MOJI STRING_TYPE;
  3  BEGIN
  4    MOJI := STRING_TYPE('AIUEO');
  5    MOJI.PRINT;  
  6    DBMS_OUTPUT.PUT_LINE('LENGTH=' || MOJI.LEN);
  7    DBMS_OUTPUT.PUT_LINE('SUBSTR(2,2)=' || MOJI.SUBSTRING(1,1));
  8  END;
  9  /
AIUEO
LENGTH=5
SUBSTR(2,2)=A

PL/SQL procedure successfully completed.