Skip to content

Commit 80a739a

Browse files
authored
Merge pull request #339 from open-edge-platform/update-branch
feat: implement wake word detection service (#887)
2 parents 77b7250 + 5a66753 commit 80a739a

File tree

56 files changed

+12218
-3542
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+12218
-3542
lines changed

usecases/ai/edge-ai-demo-studio/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ __pycache__
2020
/workers/lipsync/build
2121

2222
/workers/embedding/data
23+
/workers/wake-word-detection/data
2324

2425
/models
2526

usecases/ai/edge-ai-demo-studio/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Edge AI Demo Studio is a modern toolkit for deploying, managing, and serving AI
1919
- Lipsync
2020
- Image Generation
2121
- MCP Manager
22+
- Wake Word Detection
2223
- **Samples:** Samples use cases that implements the ai services
2324
- Digital Avatar
2425
- RAG Chat
-158 KB
Loading

usecases/ai/edge-ai-demo-studio/frontend/next.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
SPEECH_TO_TEXT_PORT,
1010
TEXT_TO_SPEECH_PORT,
1111
IMAGE_GENERATION_PORT,
12+
WAKE_WORD_DETECTION_PORT,
1213
} from '@/lib/constants'
1314

1415
const nextConfig: NextConfig = {
@@ -61,6 +62,10 @@ const nextConfig: NextConfig = {
6162
source: '/api/images/v1/:slug*',
6263
destination: `http://localhost:${IMAGE_GENERATION_PORT}/v3/images/:slug*`,
6364
},
65+
{
66+
source: '/api/wake-word-detection/:slug*',
67+
destination: `http://localhost:${WAKE_WORD_DETECTION_PORT}/:slug*`,
68+
},
6469
]
6570
},
6671
}

usecases/ai/edge-ai-demo-studio/frontend/package-lock.json

Lines changed: 289 additions & 289 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

usecases/ai/edge-ai-demo-studio/frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"graphql": "^16.11.0",
4242
"hast-util-to-jsx-runtime": "^2.3.6",
4343
"lucide-react": "^0.552.0",
44-
"next": "^15.5.7",
44+
"next": "^15.5.9",
4545
"next-themes": "^0.4.6",
4646
"openai": "^6.7.0",
4747
"openvino-node": "^2025.1.0",

usecases/ai/edge-ai-demo-studio/frontend/src/app/(frontend)/(samples)/digital-avatar/page.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export default function DigitalAvatarPage() {
3636

3737
const [sessionId, setSessionId] = useState('')
3838
const [connectionStatus, setConnectionStatus] = useState('disconnected')
39+
const [useSTT, setUseSTT] = useState(false)
40+
const [useDenoise, setUseDenoise] = useState(true)
3941
const [useEmbedding, setUseEmbedding] = useState(false)
4042
const [selectedKnowledgeBase, setSelectedKnowledgeBase] =
4143
useState<KnowledgeBase | null>(null)
@@ -45,20 +47,25 @@ export default function DigitalAvatarPage() {
4547
const prerequisiteServices = useMemo(() => {
4648
const ps = ['text-generation', 'text-to-speech', 'lipsync']
4749

50+
if (useSTT) ps.push('speech-to-text')
4851
if (useEmbedding) ps.push('embedding')
4952

5053
return ps
51-
}, [useEmbedding])
54+
}, [useEmbedding, useSTT])
5255

5356
const handleSessionIdChange = (newSessionId: string) => {
5457
setSessionId(newSessionId)
5558
}
5659

5760
const handleSettingsUpdate = (settings: {
61+
useSTT: boolean
62+
useDenoise: boolean
5863
useEmbedding: boolean
5964
selectedKnowledgeBase: KnowledgeBase | null
6065
useMcpTools: boolean
6166
}) => {
67+
setUseSTT(settings.useSTT)
68+
setUseDenoise(settings.useDenoise)
6269
setUseEmbedding(settings.useEmbedding)
6370
setSelectedKnowledgeBase(settings.selectedKnowledgeBase)
6471
setUseMcpTools(settings.useMcpTools)
@@ -217,6 +224,8 @@ export default function DigitalAvatarPage() {
217224
inactivePrerequisites.length > 0 ||
218225
(preparingPrerequisites && preparingPrerequisites.length > 0)
219226
}
227+
isSTTEnabled={useSTT}
228+
isDenoiseEnabled={useDenoise}
220229
sessionId={sessionId}
221230
connectionStatus={connectionStatus}
222231
knowledgeBaseId={selectedKnowledgeBase?.id || undefined}
@@ -232,6 +241,8 @@ export default function DigitalAvatarPage() {
232241
<DigitalAvatarSettings
233242
isOpen={isSettingsOpen}
234243
onClose={() => setIsSettingsOpen(false)}
244+
useSTT={useSTT}
245+
useDenoise={useDenoise}
235246
useEmbedding={useEmbedding}
236247
selectedKnowledgeBase={selectedKnowledgeBase}
237248
useMcpTools={useMcpTools}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
'use client'
5+
6+
import WorkloadComponent from '@/components/workloads/workload'
7+
import {
8+
useCreateWorkload,
9+
useGetWorkloadByType,
10+
useUpdateWorkload,
11+
} from '@/hooks/use-workload'
12+
13+
import useDisclosure from '@/hooks/use-disclosure'
14+
import {
15+
Model,
16+
SettingsModal,
17+
} from '@/components/workloads/wake-word-detection/settings'
18+
import { Button } from '@/components/ui/button'
19+
import { Settings } from 'lucide-react'
20+
import {
21+
WAKE_WORD_DETECTION_WORKLOAD,
22+
WAKE_WORD_DETECTION_TYPE,
23+
WAKE_WORD_DETECTION_DESCRIPTION,
24+
WAKE_WORD_DETECTION_MODELS,
25+
} from '@/lib/workloads/wake-word-detection'
26+
import WakeWordDetectionDemo from '@/components/workloads/wake-word-detection/demo'
27+
import Logs from '@/components/workloads/log'
28+
import {
29+
DocumentationProps,
30+
DocumentationTemplate,
31+
} from '@/components/workloads/documentation'
32+
import WakeWordDetectionDocumentation from '@/components/workloads/wake-word-detection/documentation'
33+
import { WAKE_WORD_DETECTION_PORT } from '@/lib/constants'
34+
import Endpoint from '@/components/workloads/endpoint'
35+
import { wakeWordDetectionEndpoints } from '@/components/workloads/wake-word-detection/api'
36+
37+
const TYPE = WAKE_WORD_DETECTION_TYPE
38+
const DESCRIPTION = WAKE_WORD_DETECTION_DESCRIPTION
39+
40+
export default function TextGenerationPage() {
41+
const { data: workload, isLoading } = useGetWorkloadByType(
42+
'wake-word-detection',
43+
)
44+
const { isOpen, onClose, onOpen } = useDisclosure()
45+
46+
const updateWorkload = useUpdateWorkload()
47+
const createWorkload = useCreateWorkload()
48+
49+
const data: DocumentationProps = {
50+
overview: (
51+
<WakeWordDetectionDocumentation port={WAKE_WORD_DETECTION_PORT} />
52+
),
53+
endpoints: (
54+
<Endpoint
55+
apis={wakeWordDetectionEndpoints}
56+
port={WAKE_WORD_DETECTION_PORT}
57+
/>
58+
),
59+
}
60+
61+
const updateSettings = (models: Model[], vadThreshold: number) => {
62+
return new Promise((resolve, reject) => {
63+
const modelString = models.map((model) => model.value).join(' ')
64+
65+
if (!workload) {
66+
createWorkload.mutate(
67+
{
68+
...WAKE_WORD_DETECTION_WORKLOAD,
69+
model: modelString,
70+
metadata: {
71+
vadThreshold,
72+
},
73+
status: 'inactive',
74+
},
75+
{
76+
onSuccess: () => resolve(true),
77+
onError: (error) => {
78+
console.error('Failed to create workload:', error)
79+
reject(error)
80+
},
81+
},
82+
)
83+
} else if (workload && !isLoading) {
84+
updateWorkload.mutateAsync(
85+
{
86+
id: workload?.id || 0,
87+
data: {
88+
model: modelString,
89+
metadata: {
90+
vadThreshold,
91+
},
92+
},
93+
},
94+
{
95+
onSuccess: () => resolve(true),
96+
onError: (error) => {
97+
console.error('Failed to update workload:', error)
98+
reject(error)
99+
},
100+
},
101+
)
102+
} else {
103+
resolve(true)
104+
}
105+
})
106+
}
107+
108+
const SettingsButton = () => {
109+
return (
110+
<Button
111+
variant="secondary"
112+
size="icon"
113+
className="size-8"
114+
onClick={onOpen}
115+
>
116+
<Settings />
117+
</Button>
118+
)
119+
}
120+
121+
return (
122+
<>
123+
<SettingsModal
124+
task="Wake Word Detection"
125+
isOpen={isOpen}
126+
onClose={onClose}
127+
predefinedModels={WAKE_WORD_DETECTION_MODELS}
128+
updateSettings={updateSettings}
129+
selectedModel={workload?.model || WAKE_WORD_DETECTION_WORKLOAD.model}
130+
vadThreshold={
131+
workload?.metadata?.vadThreshold ??
132+
WAKE_WORD_DETECTION_WORKLOAD.metadata.vadThreshold
133+
}
134+
workloadStatus={workload?.status}
135+
/>
136+
<WorkloadComponent
137+
title="Wake Word Detection"
138+
settingsButton={<SettingsButton />}
139+
workload={workload}
140+
description={DESCRIPTION}
141+
workloadType={TYPE}
142+
demoElement={
143+
<WakeWordDetectionDemo
144+
disabled={!workload || workload.status !== 'active'}
145+
/>
146+
}
147+
docsElement={<DocumentationTemplate data={data} />}
148+
logsElement={<Logs name={`${workload?.name}_${workload?.id}`} />}
149+
isLoading={isLoading}
150+
/>
151+
</>
152+
)
153+
}

usecases/ai/edge-ai-demo-studio/frontend/src/app/(frontend)/api/chat/digital-avatar/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,21 @@ class SentenceHandler {
183183
// const wordCount = countWords(sentence)
184184
// console.log(`Sentence (${wordCount} words):`, sentence)
185185

186+
// Remove special characters and emojis
187+
const processedSentence = sentence
188+
.replace(
189+
/[*#]|[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu,
190+
'',
191+
)
192+
.trim()
193+
186194
fetch(`http://localhost:${LIPSYNC_PORT}/v1/lipsync/chat`, {
187195
method: 'POST',
188196
headers: {
189197
'Content-Type': 'application/json',
190198
},
191199
body: JSON.stringify({
192-
text: sentence,
200+
text: processedSentence,
193201
session_id: sessionId,
194202
chat_type: 'echo',
195203
voice: voice,
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { NextRequest } from 'next/server'
5+
6+
const sseClients = new Set<ReadableStreamDefaultController>()
7+
8+
function broadcastEvent(data: {
9+
type: 'detection' | 'clear' | 'connected'
10+
event?: { timestamp: string; model: string; score: number }
11+
}) {
12+
const message = `data: ${JSON.stringify(data)}\n\n`
13+
sseClients.forEach((controller) => {
14+
try {
15+
controller.enqueue(new TextEncoder().encode(message))
16+
} catch (error) {
17+
console.error('Failed to send SSE message:', error)
18+
sseClients.delete(controller)
19+
}
20+
})
21+
}
22+
23+
export async function GET() {
24+
const stream = new ReadableStream({
25+
start(controller) {
26+
sseClients.add(controller)
27+
28+
const message = `data: ${JSON.stringify({ type: 'connected' })}\n\n`
29+
controller.enqueue(new TextEncoder().encode(message))
30+
31+
// Keep-alive ping every 30 seconds
32+
const keepAliveInterval = setInterval(() => {
33+
try {
34+
controller.enqueue(new TextEncoder().encode(': keep-alive\n\n'))
35+
} catch (error) {
36+
console.error('Failed to send keep-alive ping:', error)
37+
clearInterval(keepAliveInterval)
38+
sseClients.delete(controller)
39+
}
40+
}, 30000)
41+
42+
return () => {
43+
clearInterval(keepAliveInterval)
44+
sseClients.delete(controller)
45+
}
46+
},
47+
})
48+
49+
return new Response(stream, {
50+
headers: {
51+
'Content-Type': 'text/event-stream',
52+
'Cache-Control': 'no-cache',
53+
Connection: 'keep-alive',
54+
},
55+
})
56+
}
57+
58+
export async function POST(request: NextRequest) {
59+
try {
60+
const body = await request.json()
61+
console.log('Received detection:', JSON.stringify(body))
62+
63+
// Create detection event
64+
const event = {
65+
timestamp: new Date().toISOString(),
66+
model: body.model || 'Unknown',
67+
score: body.score || 0,
68+
}
69+
70+
// Broadcast to all connected SSE clients
71+
broadcastEvent({ type: 'detection', event })
72+
73+
return Response.json({
74+
success: true,
75+
message: 'Wake word detected successfully',
76+
event,
77+
})
78+
} catch (error) {
79+
let errorMessage = 'Failed to process detection'
80+
if (error instanceof Error) {
81+
errorMessage = error.message
82+
}
83+
console.error(errorMessage, error)
84+
85+
return Response.json(
86+
{
87+
success: false,
88+
message: errorMessage,
89+
data: null,
90+
},
91+
{ status: 500 },
92+
)
93+
}
94+
}

0 commit comments

Comments
 (0)