UI 연결
가장 작은 브라우저 흐름부터 시작한 뒤, 사용하는 UI 프레임워크에 맞게 확장하세요.
최소 예제
이 예제는 버튼 하나로 인식을 시작하고, 인식된 campaign name을 콘솔에 출력합니다.
html
<button id="start-button" type="button">Start Recognition</button>
<script type="module">
import { AudioRecognizer } from "@sorisdk/web-audio";
const startButton = document.querySelector("#start-button");
const recognizer = new AudioRecognizer({
appId: "YOUR_APP_ID",
ephemeralKey: async () => {
const response = await fetch("/api/ephemeral-key", {
method: "POST"
});
if (!response.ok) {
throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
}
const { ephemeral_key } = await response.json();
return ephemeral_key;
}
});
recognizer.on("campaign", (event) => {
console.log(event.campaign.name);
});
startButton?.addEventListener("click", async () => {
await recognizer.start();
});
</script>이 예제는 이미 ephemeral key endpoint가 준비되어 있다고 가정합니다. 표준 설정은 Ephemeral Key에서 확인하세요.
고급 예제
아래 예제들은 인식된 campaign을 UI에 렌더링합니다. React, Vue 등 Vite 기반 프레임워크를 사용한다면 먼저 설정 문서의 wasm 로더 구성을 적용하세요.
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SORI Web SDK</title>
<style>
#campaign-list {
padding: 0;
list-style: none;
}
#campaign-list li + li {
margin-top: 8px;
}
</style>
</head>
<body>
<main>
<button id="start-button" type="button">Start Recognition</button>
<ul id="campaign-list"></ul>
</main>
<script type="module">
import { AudioRecognizer } from "@sorisdk/web-audio";
const startButton = document.querySelector("#start-button");
const campaignList = document.querySelector("#campaign-list");
const recognizer = new AudioRecognizer({
appId: "YOUR_APP_ID",
ephemeralKey: async () => {
const response = await fetch("/api/ephemeral-key", {
method: "POST"
});
if (!response.ok) {
throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
}
const { ephemeral_key } = await response.json();
return ephemeral_key;
}
});
recognizer.on("campaign", (event) => {
console.log(event.campaign.name);
if (campaignList) {
const item = document.createElement("li");
const link = document.createElement("a");
link.href = String(event.campaign.action_url);
link.textContent = String(event.campaign.title);
link.target = "_blank";
link.rel = "noreferrer";
item.append(link);
campaignList.prepend(item);
}
});
startButton?.addEventListener("click", async () => {
await recognizer.start();
});
globalThis.addEventListener("beforeunload", () => {
void recognizer.destroy();
});
</script>
</body>
</html>tsx
import { useEffect, useRef, useState } from "react";
import { AudioRecognizer } from "@sorisdk/web-audio";
type CampaignCard = {
title: string;
imageUrl: string;
actionUrl: string;
};
export default function App() {
const recognizerRef = useRef<AudioRecognizer | null>(null);
const [campaigns, setCampaigns] = useState<CampaignCard[]>([]);
useEffect(() => {
const recognizer = new AudioRecognizer({
appId: "YOUR_APP_ID",
ephemeralKey: async () => {
const response = await fetch("/api/ephemeral-key", {
method: "POST"
});
if (!response.ok) {
throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
}
const { ephemeral_key } = await response.json();
return ephemeral_key;
}
});
recognizer.on("campaign", (event) => {
console.log(event.campaign.name);
setCampaigns((current) => [
{
title: String(event.campaign.title),
imageUrl: String(event.campaign.image_url),
actionUrl: String(event.campaign.action_url)
},
...current
]);
});
recognizerRef.current = recognizer;
return () => {
void recognizer.destroy();
recognizerRef.current = null;
};
}, []);
return (
<main>
<button
type="button"
onClick={() => {
void recognizerRef.current?.start();
}}
>
Start Recognition
</button>
<div>
{campaigns.map((campaign, index) => (
<a
key={`${campaign.title}-${index}`}
href={campaign.actionUrl}
target="_blank"
rel="noreferrer"
>
<img src={campaign.imageUrl} alt={campaign.title} />
<strong>{campaign.title}</strong>
</a>
))}
</div>
</main>
);
}vue
<script setup lang="ts">
import { onBeforeUnmount } from "vue";
import { ref } from "vue";
import { AudioRecognizer } from "@sorisdk/web-audio";
const campaigns = ref<
Array<{ title: string; imageUrl: string; actionUrl: string }>
>([]);
const recognizer = new AudioRecognizer({
appId: "YOUR_APP_ID",
ephemeralKey: async () => {
const response = await fetch("/api/ephemeral-key", {
method: "POST"
});
if (!response.ok) {
throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
}
const { ephemeral_key } = await response.json();
return ephemeral_key;
}
});
recognizer.on("campaign", (event) => {
console.log(event.campaign.name);
campaigns.value.unshift({
title: String(event.campaign.title),
imageUrl: String(event.campaign.image_url),
actionUrl: String(event.campaign.action_url)
});
});
async function startRecognition() {
await recognizer.start();
}
onBeforeUnmount(() => {
void recognizer.destroy();
});
</script>
<template>
<main>
<button type="button" @click="startRecognition">Start Recognition</button>
<div class="campaign-grid">
<a
v-for="campaign in campaigns"
:key="`${campaign.actionUrl}-${campaign.title}`"
:href="campaign.actionUrl"
target="_blank"
rel="noreferrer"
class="campaign-card"
>
<img :src="campaign.imageUrl" :alt="campaign.title" />
<strong>{{ campaign.title }}</strong>
</a>
</div>
</main>
</template>
<style scoped>
.campaign-grid {
display: grid;
gap: 12px;
margin-top: 16px;
}
.campaign-card {
display: grid;
gap: 8px;
color: inherit;
text-decoration: none;
}
</style>svelte
<script lang="ts">
import { onMount } from "svelte";
import { AudioRecognizer } from "@sorisdk/web-audio";
let campaigns: Array<{ title: string; imageUrl: string; actionUrl: string }> = [];
let recognizer: AudioRecognizer;
onMount(() => {
recognizer = new AudioRecognizer({
appId: "YOUR_APP_ID",
ephemeralKey: async () => {
const response = await fetch("/api/ephemeral-key", {
method: "POST"
});
if (!response.ok) {
throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
}
const { ephemeral_key } = await response.json();
return ephemeral_key;
}
});
recognizer.on("campaign", (event) => {
console.log(event.campaign.name);
campaigns = [
{
title: String(event.campaign.title),
imageUrl: String(event.campaign.image_url),
actionUrl: String(event.campaign.action_url)
},
...campaigns
];
});
return () => {
void recognizer.destroy();
};
});
async function startRecognition() {
await recognizer.start();
}
</script>
<main>
<button type="button" on:click={startRecognition}>Start Recognition</button>
<div class="campaign-grid">
{#each campaigns as campaign}
<a
href={campaign.actionUrl}
target="_blank"
rel="noreferrer"
class="campaign-card"
>
<img src={campaign.imageUrl} alt={campaign.title} />
<strong>{campaign.title}</strong>
</a>
{/each}
</div>
</main>
<style>
.campaign-grid {
display: grid;
gap: 12px;
margin-top: 16px;
}
.campaign-card {
display: grid;
gap: 8px;
color: inherit;
text-decoration: none;
}
</style>참고 사항
- 마이크 권한 요청은 버튼 클릭 같은 사용자 액션에서 시작하세요.
- 페이지나 컴포넌트가 제거될 수 있다면 cleanup 시점에
destroy()를 호출하세요. - 더 복잡한 제어 흐름이 필요하다면, 이 최소 연결이 동작한 뒤
stop()과 추가 이벤트 핸들러를 붙이는 편이 좋습니다.
