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에서 확인하세요.
현재 활동 세그먼트 조정
Web SDK가 서버 활동 정제 전송을 담당하지만 렌더링된 타임라인은 애플리케이션이 관리합니다. 공개 이벤트 스트림을 도착 순서대로 사용하세요.
match이벤트는best.name을 제공합니다. 다른 소재 이름은 해당 match가 캠페인 카드를 생성하지 않더라도 즉시 경계가 됩니다.- 최초
campaign이벤트는 즉시 표시하세요. 이 이벤트는event.activityId,event.match.name,event.match.position,event.campaign을 제공합니다. - 비어 있지 않은
event.activityId가 같은 후속 캠페인은 현재 행만 정제할 수 있습니다. 활동 ID가 null이면 소재 이름 fallback은 현재 행에만 안전하며 전체 이력을 검색하면 안 됩니다. - 최초 행의 시각과
event.match.position을 유지하세요. 이후 marker miss 또는 덜 완전한 결과가 행을 이전 상태로 되돌리지 않도록 최초 marker와 정제된 캠페인 필드를 병합하세요. audiomarker이벤트는 독립적으로 관측할 수 있는 marker 상태이며 이 이벤트만으로 캠페인 행을 정제하지 않습니다. 다른match,nomatch,stop,destroy는 현재 세그먼트를 닫습니다.
SORI Console이 확인한 마커 ID와 이름은 event.marker.id와 event.marker.name에서 읽으세요.
따라서 A(activity-1) -> A'(activity-1, marker) -> A(activity-1, marker miss)는 하나의 행입니다. A(activity-1) -> B(activity-2) -> delayed A'(activity-1)에서는 B가 현재 상태로 유지되고 과거 A에 대한 지연된 변경은 무시됩니다.
고급 예제
아래 예제들은 인식된 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()과 추가 이벤트 핸들러를 붙이는 편이 좋습니다.
