AWSを使った「なろう」をPollyで朗読させる
前回は「なろう」WebAPIを作りましたが、今回はAWSの読み上げサービスPollyで 取得したチャプターを読み上げた音声ファイルをS3に保存してみます。
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
AWSを使った「なろう」WebAPIの作成
「蜘蛛ですが何か?」が面白いと妻から聞いたので「なろう」のWebAPIをAWSで作ってみました。
構成
CloudFront=>APIGateway=>Lambda=>SocksProxy=>なろうサイト
- CloudFront
- キャッシュサーバ SocksProxyを経由したために遅くなったレスポンスの改善&なろうサイトのサーバに負荷をかけないため設置
- キャッシュに載せるには MethodをGetにする必要あり
- APIGateway
- HttpRequest受け口
- path paramter を取得してeventへセットしてくれるよう設定
- Lambda
- SocksProxy
- なろうサイト
- タイトルと本文を取得
リクエストサンプル
path parameterに本のIDと章を渡すとJSONでtitleとcontents(行単位のarray)が返却されます。
下記は蜘蛛ですが何かのの一つ目がJSONで返却されるサンプル(そのうち止めます)
curl https://d1oy3e79o7znce.cloudfront.net/fastread/n7975cr/1
上記URLに対応するなろうサイトのURLはこちら。
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
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
- typeofとinstanceofについてまとめ tweeeety.hateblo.jp
obj.hasOwnProperty(key)
paths.concat
path ? path + "." + key : key
動作サンプル
これ
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.