Wiring UI
Start with the smallest possible browser flow, then adapt it to your UI framework.
Smallest Example
This example starts recognition from a single button and logs the recognized campaign name.
<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>This example assumes you already have an ephemeral key endpoint. See Ephemeral Key for the standard setup.
Reconcile the current activity segment
The Web SDK owns the server activity refinement transport, but the application still owns its rendered timeline. Use the public event stream in arrival order:
- The
matchevent exposesbest.name. A different material name is an immediate boundary, even when that match does not produce a campaign card. - Show the first
campaignevent immediately. It exposesevent.activityId,event.match.name,event.match.position, andevent.campaign. - A later campaign with the same non-empty
event.activityIdmay refine only the current row. If the activity ID is null, a material-name fallback is safe only for that current row; never search the full history. - Retain the first row's timestamp and
event.match.position. Merge the first marker and refined campaign fields so a later marker miss or less complete result cannot downgrade the row. - An
audiomarkerevent is independently observable marker state and does not refine a campaign row by itself. A differentmatch,nomatch,stop, ordestroyseals the current segment.
Read the Console-resolved marker ID and name from event.marker.id and event.marker.name.
Thus A(activity-1) -> A'(activity-1, marker) -> A(activity-1, marker miss) is one row. A(activity-1) -> B(activity-2) -> delayed A'(activity-1) keeps B current and ignores the delayed rewrite of historical A.
Advanced Examples
The following examples render recognized campaigns in the UI. If you use React, Vue, or another Vite-based framework, configure the wasm loader in Setup first.
<!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>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>
);
}<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><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>Notes
- Request microphone access only from a user action such as a button click.
- If your page or component can be removed, call
destroy()during cleanup. - If you need a richer control flow, add
stop()and other event handlers after this minimal wiring works.
