こんにちは。株式会社ウフルの廣田です。
この記事は「2025 Japan AWS Jr. Champions 夏のQiitaリレー」の14日目です。
過去の投稿(リンク集)はこちらからご覧ください!
-
AWS AppSyncとは
サーバーレスで高性能なGraphQLおよびPub/Sub APIsを使用して、アプリケーションとサービスをイベントに接続できるサービスです。[1]
元々はGraphQLのイメージが強かったですが、2024年10月にAppSync Event APIがリリースされました。
こちらはあらゆる規模のリアルタイムウェブ体験およびモバイル体験を強化する、安全で高性能なサーバーレス WebSocket API を構築するためのソリューションを提供しているようです。[2] -
課題、願望
Amazon Connect の文字起こしをリアルタイムで画面に表示させたら便利だよな〜と思っていました! -
今回やってみること!
「AppSync Event API のWebSocket APIを使用して、Amazon Connect Contact Lensによってリアルタイムに文字起こしされた情報をブラウザに表示する」ことを実装してみました!
ブラウザに表示することができるということは、他のSaaSなどに文字起こし内容をリアルタイムで表示することが可能になると考えています。
この記事の内容が誰かの助けになれば幸いです。
今回はAmazon Connect 内のストリーミング情報をKinesisで受け取り、LambdaにEvent情報として渡します。
Lambda側ではブラウザ側で通話者間のやりとりがわかりやすいように整形して、AWS AppSyncへ渡し、そのままブラウザに表示する流れです。
"""
Kinesis(Contact Lens Streams) → Lambda → AppSync Event API へ API キー認証でイベントを Publish する関数
環境変数
HTTP_ENDPOINT = ".appsync-api..amazonaws.com "
API_KEY = "da2-xxxxxxxxxxxxxxxxxxxx" # AWS AppSync で発行したキー
CHANNEL = "/default/message" # チャンネル名
"""
import botocore.awsrequest, botocore.httpsession, json, os, base64
HTTP_ENDPOINT = os.environ["HTTP_ENDPOINT"].rstrip("/")
API_KEY = os.environ["API_KEY"]
CHANNEL = os.environ["CHANNEL"]
session = botocore.httpsession.URLLib3Session()
def lambda_handler(event, context):
try:
print(f"Received event: {event}")
segments = []
for record in event.get("Records", []):
payload = record["kinesis"]["data"]
decoded = base64.b64decode(payload)
data = json.loads(decoded)
print(f"Decoded data: {data}")
if "Segments" in data:
segments.extend(data["Segments"])
else:
print("No Segments found in the record.")
transcript = correct_transcript(segments)
events = [
json.dumps({"role": t["role"], "content": t["content"]})
for t in transcript
]
print(f"AppSync Post Event: ", events)
body = _http_post({"channel": CHANNEL, "events": events})
return {"statusCode": 200, "body": body}
except Exception as e:
print(f"Error processing event: {e}")
return {"statusCode": 500, "body": str(e)}
def correct_transcript(segments: list):
corrected_segments = []
role_mapping = {
"AGENT": "エージェント",
"CUSTOMER": "顧客",
}
for s in segments:
if "Transcript" in s:
transcript = s["Transcript"]
content = transcript.get("Content", "")
participant_role = transcript.get("ParticipantRole", "")
role = role_mapping.get(participant_role)
begin_offset = transcript.get("BeginOffsetMillis", 0)
if role and content:
corrected_segments.append({
"role": role,
"content": content,
"beginOffsetMillis": begin_offset
})
corrected_segments.sort(key=lambda x: x["beginOffsetMillis"])
return corrected_segments
def _http_post(payload: dict) -> str:
req = botocore.awsrequest.AWSRequest(
method="POST",
url=f"https://{HTTP_ENDPOINT}/event",
data=json.dumps(payload).encode(),
headers={
"content-type": "application/json",
"x-api-key": API_KEY
}
)
resp = session.send(req.prepare())
if resp.status_code >= 400:
raise RuntimeError(f"Publish failed: {resp.status_code} {resp.text[:200]}")
return resp.text or "OK"
lang="ja">
リアルタイムの文字起こし
// ハードコーティングになってしまってすみません。こちらは適宜環境変数に置き換えてください。
const HTTP_ENDPOINT = ".appsync-api..amazonaws.com/event ";
const WS_ENDPOINT = ".appsync-api..amazonaws.com ";
const API_KEY = "da2-xxxxxxxxxxxxxxxxxxxx";
const CHANNEL = "/default/message";
// util
function b64url(str){return btoa(str).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');}
function addLog(text){document.querySelector("#log").insertAdjacentHTML("beforeend",`${text}`);}
// --------------------------------------------------
// WebSocket 接続
const header = b64url(JSON.stringify({host: HTTP_ENDPOINT.replace("/event",""), "x-api-key": API_KEY}));
const ws = new WebSocket(`wss://${WS_ENDPOINT}/event/realtime`,
['aws-appsync-event-ws', `header-${header}`]);
ws.onopen = () => { console.log("🟢 OPEN"); ws.send(JSON.stringify({type:"connection_init"})); };
ws.onerror = e => console.error("🔴 WS error", e);
ws.onclose = e => console.warn(`🟠 WS closed: ${e.code}`);
ws.onmessage = ev=>{
console.log("📩", ev.data);
const m = JSON.parse(ev.data);
if (m.type === "connection_ack") {
// 接続ACK → 購読
ws.send(JSON.stringify({
type:"subscribe",
id:crypto.randomUUID(),
channel:CHANNEL,
authorization:{host:HTTP_ENDPOINT.replace("/event",""), "x-api-key":API_KEY}
}));
}
if (m.type === "subscribe_success") console.log("✅ subscribed");
// ★ Event API は "data" フレームにセットされてくる
if (m.type === "data" && m.event) {
const payload = JSON.parse(m.event);
addLog(`[${payload.role}] ${payload.content}`);
}
};
Source link
Views: 0