ã¯ããã« ããã«ã¡ã¯ïŒæ°åå
¥ç€Ÿ4幎ç®ã®å°æŸã§ããäž»ã«ã客æ§ãåããŠ@niftyããå©çšã«ãªãéã®ç¡æIDäŒå¡ç»é²ã·ã¹ãã ããããããªãµãŒãã¹ããå©çšã«ãªãéã®ãã°ã€ã³ã·ã¹ãã ã®éçºã»éçšãæ
åœããŠããŸãã ä»åã¯ãªã¢ãŒãäŒè°ã®ãªã¢ã¯ã·ã§ã³ãããã¥ããåé¡ãè§£æ¶ããããŒã«ãããããããäœã£ãŠã¿ãã®ã§ã玹ä»ããããšæããŸãã瀟å
ã§ãäžéšã®äŒè°ã§ã¯å®éã«äœ¿ãããŠããŸãã ãªã¢ãŒãäŒè°ã¯ãªã¢ã¯ã·ã§ã³ããããã¥ãã 察é¢ã§ã®äŒè°ãšéã£ãŠãåå è
ã®ãªã¢ã¯ã·ã§ã³ããããã¥ãããšæãããšãã¯ãããŸãããã ã«ã¡ã©ã»ãã€ã¯ããªãã®æã¯ãã¡ãããããªãããç¬ã声ãªã©äºçްãªãªã¢ã¯ã·ã§ã³ããããã¥ãããäŒãã£ãŠããã®ãäžå®ã«ãªãããšããããŸãã ãŸãå®éã«ã¯ã¯ã€ã¯ã€ãšããŠããäŒè°ããã³ã¡ã³ããæŸãå¿ããããçãäžããã«æ¬ ãããªãšæããããšããããŸãã ããã§ãããããããäœããŸããã ãããã AIé³å£°ã§ã³ã¡ã³ãèªã¿äžããŠãããå¿åãã£ããããŒã«Webã¢ããªã§ãã äœ¿ãæ¹ã¯ããªã¢ãŒãäŒè°æã«åå è
ãããããããããã©ãŠã¶ã§éããŠãã£ããããŸãã èªã¿äžãé³å£°ã¯çŸåš3çš®é¡ã§ãã³ã¡ã³ãæçš¿ããšã«åãæ¿ãã§ããŸãã å®éã«äœ¿ã£ãŠã¿ãªããšäŒããã¥ããããšæããŸãããç»é¢ã¯ãããªæãã§ãã ãŸã ãã¶ã€ã³ãèãäžãªã®ã§ããããã¿ã€ãã«ãªã£ãŠããŸãã ã¡ãªã¿ã«ååã®ç±æ¥ã¯ã GoogleMeetæ¡åŒµæ©èœã®ãããããã ãšãããã®ããããŸããŠãMeetã§è©±ããå
容ãæåèµ·ããããŠãã³ã¡ã³ãæ¬ã«è¿œå ããŠããããã®ã§ãã ãã®éïŒããã¹ãâé³å£°ïŒãããã°ãšæã£ãã®ãå§ãŸãã§ããã 詊ãã«ç€Ÿå
ã«å
¬éããŠã¿ãŸããããæå€ãšåå¿ãããã瀟å
ã®LT倧äŒãå匷äŒãªã©ã§ã䜿ãããŠããŸããã¡ãªã¿ã«ç§ãæå±ãã ãµãŒãã¹ã€ã³ãã©ããŒã ã§ã¯ãæ¯æ¥äœ¿ã£ãŠããŸãã ãããã ã®äžèº« æ§æ ããã¯ãšã³ãã¯Node.js + Expressã§ãSocket.IOã䜿ã£ãŠãªã¢ã«ã¿ã€ã ãã£ããã§ããããã«ããŠããŸããé³å£°å€æã¯AWS SDKã®Pollyã䜿ã£ãŠããŸãã ããã³ãã¯çHTMLãšJQueryã§ãããã¶ã€ã³ã¯ããšãŸããã«ããŠããã®ã§ãã®ã¡ã®ã¡Reactãªã©ã§äœãçŽããããšæã£ãŠããŸãã ããããAWSã®Fargateã§åãããŠããŸãã åŸã»ã©ç޹ä»ããSlack飿ºã§ã¯ãSlack Appãäœæãã Lambdaã䜿çšããŠããŸãã èªã¿äžãé³å£°ã¯AWSã®Amazon Pollyãäœ¿çš Amazon Polly ã¯ããã¹ããé³å£°ã«å€æããŠãããAWSã®ãµãŒãã¹ã§ããæ§ã
ãªèšèªã«å¯Ÿå¿ããæ¥æ¬èªã«ã察å¿ããŠããŸãã æéã¯ã100äžæåããšã«4ãã«ãªã®ã§ããããŸã§æ°ã«ãã䜿ããããªãšæã£ãŠããŸããä»ãŸã§ã§æé«ã§ã3ãã«æªæºã§ããã ãããã ã«ã¯ã3çš®é¡ã®é³å£°ãæ¡ç𠿥æ¬èªã話ãã3çš®é¡ã®é³å£°ãæ¡çšããŸãããçµæ§ãªã¢ã«ã§ãã äžçªäººæ°ã¯ãã·ã¥ãŒã§ãã ããºãïŒå¥³æ§ïŒ ã¿ã¯ãïŒç·æ§ïŒ ãã·ã¥ãŒ(å€åœç±ã§æ¥æ¬èªã話ããç·æ§) ãã·ã¥ãŒã®é³å£°ãµã³ãã« ãœãŒã¹çŽ¹ä» ãŸã ãªããžããªãå
¬éã§ããç¶æ
ã§ã¯ãªããããäžéšã玹ä»ããŠãããŸãã ããã¹ãâé³å£°å€æ éšå Node.js + Expressã®ãµãŒãåŽã§ãAWS SDKã§Pollyã䜿çšããŸãã import AWS from "aws-sdk"; // ãã®åœ¢åŒã®ãã¡ã€ã«ãäœã£ãŠãåèªã®å€ã§åããã // { "accessKeyId": "", "secretAccessKey": "", "region": "" } AWS.config.loadFromPath('config.json'); const textToSpeakUrl = async (text, voiceId) =>{ // 決ããæåæ°ä»¥éã¯ãçç¥ãã«å€æ text = omitLongText(text); // URLã¯ãURLçç¥ãã«å€æ text = replacementUrl(text); // Create the JSON parameters for getSynthesizeSpeechUrl const speechParams = { OutputFormat: "mp3", SampleRate: "16000", // èªãŸããããã¹ã Text: text, TextType: "text", // "Mizuki"ãªã©ãå
¥ãã VoiceId: voiceId }; let speakUrl; // Pollyæºå const polly = new AWS.Polly({apiVersion: '2016-06-10'}); const signer = new AWS.Polly.Presigner(speechParams, polly); // é³å£°URLã«å€æ signer.getSynthesizeSpeechUrl(speechParams, function(error, url) { if (error) { return "" } else { speakUrl = url; } }); return speakUrl; } getSynthesizeSpeechUrl ã䜿ã£ãŠãããã¹ããé³å£°URLã«å€æããŸãã ããã¹ãã®çç¥ã¯ãé·ãããæç« ãšãURLã¯èæ
®ããŠè¡ã£ãŠããŸãã ãã®ãŸãŸã ãšãURLã¯åŸåã«1æåãã€èªã¿äžããŠãããŸãã ãã ããåã¯ã©ã€ã¢ã³ãåŽã§è¡šç€ºãããããã¹ãã¯çç¥ãããã®ãŸãŸã§ãé³å£°å€ææã®ã¿çç¥ãããããã¹ãã䜿ããŸãã ãã£ããéšå Socket.IOã䜿ã£ããã£ããèªäœã¯ããããäœ¿ãæ¹ã§ããåçŽã«ããã¹ããšãé³å£°å€æããé³å£°URLãåéšå±ã®ã¯ã©ã€ã¢ã³ãåŽã«éä¿¡ããŠããã ãã§ãã ãµãŒããŒåŽ io.on('connection',function(socket){ // éšå±å
¥å®€ socket.on('enterTheRoom', function({roomId: roomId}){ socket.join(roomId); }) // ããã¹ããåä¿¡ããããããã¹ããšé³å£°URLãéä¿¡ socket.on('speakTextRoom',async function(speakInfo){ const {speakText, voiceId, roomId, channelId} = speakInfo; // ããã¹ããé³å£°URLã«å€æ const speakUrl = await textToSpeakUrl(speakText, voiceId); const speakData = {speakText: speakText, speakUrl: speakUrl}; console.log(JSON.stringify({roomId: roomId, speakData: speakData})); // channelId ãããã°ãSlackéä¿¡ããŸãã if (channelId !== '') { // Slackã«éä¿¡ postSlackMsg(channelId, voiceId, speakData); } io.to(roomId).emit('speakData', speakData); }); }); ããã¯ã·ã³ãã«ã§ãããåãåã£ãããã¹ããé³å£°URLã«å€æããŠãã¯ã©ã€ã¢ã³ãåŽã«éä¿¡ããŸãã channelId ãããã°ãSlackã«ãéä¿¡ããŸãã ã¯ã©ã€ã¢ã³ãåŽ var socketio = io(); $(function () { // éšå±çªå·ååŸ const pathname = location.pathname; const roomId = pathname.split("room/").pop(); let channelId = ""; // éšå±å
¥å®€ socketio.emit("enterTheRoom", {roomId: roomId}); // éä¿¡ $("#message_form").submit(function () { // 空çœã空æåã¯éä¿¡ããªãã if ($("#input_msg").val().trim().length == 0 ) { $("#input_msg").val(""); return false; } // Slack飿ºflgãcheckedãªããchannel_idãæž¡ãã if ((document.getElementById('slack_link')).checked) { // å
é ãCã§ã¯ãªããšã""ã«(channelId) if (roomId.slice(0, 1) === 'C') { channelId = roomId; } } else { channelId = ""; } // ãµãŒãã«éä¿¡ socketio.emit("speakTextRoom", { speakText: $("#input_msg").val(), voiceId: $("#voiceId").val(), roomId: roomId, channelId: channelId} ); $("#input_msg").val(""); return false; }); // ããã¹ããšé³å£°URLãåä¿¡ socketio.on("speakData", function (speakData) { $("#messages").append($("<li>").text(speakData["speakText"])); const music = new Audio(speakData['speakUrl']); // é³é music.volume = Number($("#volume").val()); music.play(); // èªåã¹ã¯ããŒã«ã«ãã£ãã¯ãªããã°ã ã¹ã¯ããŒã«ããªã if ((document.getElementById('auto_scroll_flg')).checked) { $('.message_area').animate({scrollTop: $('.message_area')[0].scrollHeight}, 'fast'); } }); }); URLãã¹( /room/{åéšå±çªå·} )ããšã«éšå±ãåããããã«ããŠããã®ã§ãåéšå±ã«å
¥å®€ãããŸãã Slack飿ºã«ã€ããŠã¯ãåŸè¿°ããŸãã é³å£°URLã®åçã¯ã·ã³ãã«ã§ã Audio() ã䜿ã£ãŠããŸãã Slack飿º Slack飿ºãããããã«ããèæ¯ã¯ããããã£ã§ã¯ç¹å®ã®ãªã¢ãŒãäŒè°(倧人æ°ãåå ããã€ãã³ããªã©)ã§ã¯Slack宿³ãã£ã³ãã«ãšãããã®ãååšããŠããŠã宿³ã®åæããªããããã§ãã çŸæç¹ã§ã¯ããããããããSlackã®åæ¹åã«ã³ã¡ã³ããããããã®ã³ã¡ã³ããæµããããã«ããŸããã ãããã â Slackã®ãã£ã³ãã« éšå±çªå·ãSlackã®ãã£ã³ãã«IDã«ããããšã§ã( /room/{Slackã®ãã£ã³ãã«ID} )ã§ãã£ã³ãã«IDãååŸããŸããSlack飿ºã«ãã§ãã¯ãã€ããã°ããµãŒãåŽã§webhookã䜿ã£ãŠSlackãã£ã³ãã«ã«éä¿¡ããŸãã Slackã«POSTããããŒã¿ã¯ä»¥äžã§ããçŸç¶ããããSlackã§é³å£°åçã¯ã§ããªãã®ã§ãããåçãã¿ã³ã®çµµæåãæŒãã°ãé³å£°URLãéãä»çµã¿ã«ããŠããŸãã const payload = JSON.stringify({ channel: channelId, username: username, icon_emoji: iconEmoji, attachments: [ { color: "#ffdbb7", text: `${speakText}\n<${speakUrl}|:arrow_forward:>`, footer: `<{ãã¡ã€ã³}/room/${channelId}|fromãããã>`, }, ], }); Slackã®ãã£ã³ãã« â ãããã ãã¡ãã¯ãSlack AppãäœæããŠãã¡ãã»ãŒãžããŒã¿ãåããããã«ããŸããã ããŒã¿ã®ååå
ã¯æè»œãªAWS Lambda + API Gatewayã«ããŸããã Lambda + API Gateway APIGatewayã¯Lambdaã®APIåã«äœ¿ã£ãã ãã§ãç¹ã«çããããšãããŠããªãã®ã§ã説æã¯çããŸãã LambdaãœãŒã¹ã¯ä»¥äžã«ãªããŸãã import json import logging import urllib.request import urllib.parse logger = logging.getLogger() logger.setLevel(logging.INFO) def lambda_handler(data, context): logger.info(json.dumps(data)) if ('challenge' in data): return { "statusCode": 200, "body": json.dumps({'challenge': data['challenge']}) } # ããããçš if data.get("event") is not None: subtype = data["event"].get("subtype") # ãããã ãªã©ãbotããéä¿¡ãããã¡ãã»ãŒãžã¯äœãããreturn if subtype == "bot_message": return text = data["event"].get("text") roomId = data["event"].get("channel") mojikoe_direct_posting(roomId, text) return { "statusCode": 200 } def mojikoe_direct_posting(roomId, text): params = urllib.parse.urlencode({'room': roomId, 'text': text}) url = '{ãã¡ã€ã³}/api/direct-posting?%s' %params req = urllib.request.Request(url) urllib.request.urlopen(req) return "ok" Slackã®Requestãåãåãã«ã¯Slack Appã§æ¿èªãããå¿
èŠããããŸãããªã¯ãšã¹ãããŒã¿ã« challenge ãå«ãŸããŠããããç¹å®ã®ã¬ã¹ãã³ã¹ãè¿ãããã«ããŸãã ãŸãåæ¹åã®å®è£
ããã«ããããèæ
®ããå¿
èŠããã£ãã®ããããããããâSlackã«éä¿¡ãããå ŽåãSlack Appã®Eventãçºç«ãããããããããâ Slack â ããããããã®ããã«åã³éä¿¡ããããšããããšã§ãã 以äžã®ããã«ãSlackããPOSTãããããŒã¿ã® event ã® subtype ãèŠããšã bot_message ã«ãªã£ãŠããã®ãããã£ãã®ã§ãããã§å€å¥ããããã«ããŸãããã¡ãªã¿ã«ãæ®éã«Slackã«æçš¿ããå Žå㯠"type": "message" ã«ãªããŸãã ~~~ "event": { "type": "message", "subtype": "bot_message", "text": "ãããã ãã Slack ã«", "ts": "1661256966.757159", "username": "Mizuki", "icons": { "emoji": ":woman:" }, ~~~ ããããããã«éä¿¡ããéšåã«ã€ããŠã¯ãããããããã®ãµãŒãåŽã§ç°¡åãªAPIãäœæããã®ã§ããããåŒã¶ã ãã§ãã ãŸããµãŒãåŽã§éšå±åãšã¡ãã»ãŒãžãåãåããé³å£°ã¯ã©ã³ãã ã«ããŠãé³å£°URLãäœæãã察象ã®éšå±ã«éä¿¡ããŸãã ãµãŒãåŽã®ãœãŒã¹ // API app.get('/api/direct-posting' , async function(req, res){ const roomId = req.query.room; const voiceIds = ["Mizuki", "Matthew", "Takumi"] const voiceId = voiceIds[Math.floor(Math.random() * voiceIds.length)]; const speakUrl = await textToSpeakUrl(req.query.text, voiceId); const speakData = {speakText: req.query.text, speakUrl: speakUrl} io.to(roomId).emit('speakData', speakData); console.log(roomId, speakData); res.status(200); res.send("ok"); }); Slack Appã®èšå® Subscribe to bot events ã§ message.channels ã远å ãããšãäœæããSlack Appã远å ãããŠãããã£ã³ãã«ã§ã¡ãã»ãŒãžãæçš¿ããããšãã«ã飿ºããURLã«ããŒã¿ãPOSTããŠãããŸãã â»å€éšã«Slackã¡ãã»ãŒãžãéä¿¡ãããã®ã§ãæ±ãã«ã¯æ°ãã€ããå¿
èŠããããŸãã Lambda + API Gatewayã§åãåã Request URL ã«è¿œå ããŠé£æºããŸããäžã§èª¬æãããç¹å®ã®ã¬ã¹ãã³ã¹ãè¿ãçšã«å®è£
ããŠãããšæ¿èªãããŸãã æºåãã§ããã®ã§ãããšã¯é£æºããããã£ã³ãã«ã«Slack AppãæåŸ
ã ããããããã®URLã /room/{Slackã®ãã£ã³ãã«ID} ã«ããŠãSlack飿ºã®ãã§ãã¯ããããã°ã宿ã§ãã ä»åŸããããš ããšããšå匷ã§äœãå§ãããã®ã§ããŸã ãŸã ããããšããã£ã±ããããŸãã ããã³ããReactãªã©ã§æžãæãã Slack飿ºã®ãªã³ãªãå¶åŸ¡ 瀟å
SSO飿º çŸåšå¿åã§ã®ã³ã¡ã³ãã«ãªãã®ã§ãããŒã ã§çžè«ããŠãã°æŽåã¯å¿
èŠãšããããšã«ãªããŸããã Meetã®æ¡åŒµæ©èœã«ã§ããªããèª¿æ» ãªã©ãªã© ãŸããªã«ãé²å±ãããã°ãããã°ã«æžããããšæã£ãŠãŸãïŒ ãåç¥ãNIFTY Tech Dayã§ããããããã«ã€ããŠè©±ããããããŸããïŒ NIFTY Tech Day 2022 We are hiring! ãããã£ã§ã¯ãããŸããŸãªãããã¯ããžææŠãããšã³ãžãã¢ãçµ¶è³åéäžã§ãïŒ ãèå³ã®ããæ¹ã¯ä»¥äžã®æ¡çšãµã€ããããæ°è»œã«ãé£çµ¡ãã ããïŒ ãããã£æ ªåŒäŒç€Ÿæ¡çšæ
å ± Tech TalkãMeetUpãéå¬ããŠãããŸãïŒ ãã¡ãããæ°è»œã«ãå¿åãã ããïŒ Event â NIFTY engineering