Skip to content

Commit 374264b

Browse files
authored
Web demo improvements (#1377)
1 parent 14b93de commit 374264b

File tree

10 files changed

+713
-136
lines changed

10 files changed

+713
-136
lines changed

.github/workflows/web-demos.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
strategy:
2727
matrix:
28-
node-version: [16.x, 18.x, 20.x]
28+
node-version: [18.x, 20.x, 22.x]
2929

3030
steps:
3131
- uses: actions/checkout@v3

demo/web/.prettierignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Ignore artifacts:
2+
build
3+
coverage

demo/web/.prettierrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

demo/web/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ Signup or Login to [Picovoice Console](https://console.picovoice.ai/) to get you
1414

1515
## Install & run
1616

17-
Use `yarn` or `npm` to install the dependencies, and the `start` script with a language code
18-
to start a local web server hosting the demo in the language of your choice (e.g. `de` -> German, `ko` -> Korean).
17+
Use `yarn` or `npm` to install the dependencies, and the `start` script with a language code
18+
to start a local web server hosting the demo in the language of your choice (e.g. `de` -> German, `ko` -> Korean).
1919
To see a list of available languages, run `start` without a language code.
2020

2121
```console

demo/web/eslint.config.mjs

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import globals from "globals";
2+
import pluginJs from "@eslint/js";
3+
4+
/** @type {import('eslint').Linter.Config[]} */
5+
export default [
6+
{ files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
7+
{
8+
languageOptions: {
9+
globals: {
10+
...globals.browser,
11+
...globals.node,
12+
porcupineKeywords: "readonly",
13+
porcupineModel: "readonly",
14+
PorcupineWeb: "readonly",
15+
},
16+
},
17+
},
18+
pluginJs.configs.recommended,
19+
{
20+
rules: {
21+
"no-unused-vars": ["off"],
22+
},
23+
},
24+
];

demo/web/index.html

+57-117
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,62 @@
1-
<!DOCTYPE html>
1+
<!doctype html>
22
<html lang="en">
3-
<head>
4-
<script src="node_modules/@picovoice/web-voice-processor/dist/iife/index.js"></script>
5-
<script src="node_modules/@picovoice/porcupine-web/dist/iife/index.js"></script>
6-
<script src="keywords/porcupineKeywords.js"></script>
7-
<script src="models/porcupineModel.js"></script>
8-
<script type="application/javascript">
9-
10-
let porcupine = null;
11-
12-
window.addEventListener('load', function () {
13-
let usingBuiltIns = false;
14-
if(porcupineKeywords.length === 0 && porcupineModel.publicPath.endsWith("porcupine_params.pv")) {
15-
usingBuiltIns = true;
16-
for(const k in PorcupineWeb.BuiltInKeyword) {
17-
porcupineKeywords.push(k);
18-
}
19-
}
20-
21-
let select = document.getElementById("keywords");
22-
for(let i = 0; i < porcupineKeywords.length; i++) {
23-
let el = document.createElement("option");
24-
el.textContent = usingBuiltIns ? PorcupineWeb.BuiltInKeyword[porcupineKeywords[i]] : porcupineKeywords[i].label;
25-
el.value = `${i}`;
26-
select.appendChild(el);
27-
}
28-
});
29-
30-
function writeMessage(message) {
31-
console.log(message);
32-
document.getElementById("status").innerHTML = message;
33-
}
34-
35-
function porcupineErrorCallback(error) {
36-
writeMessage(error);
37-
}
38-
39-
function porcupineKeywordCallback(detection) {
40-
const time = new Date();
41-
const message = `keyword detected at ${time.toLocaleTimeString()}: ${detection.label} (index = ${detection.index})`
42-
console.log(message);
43-
document.getElementById("result").innerHTML = message;
44-
}
45-
46-
async function startPorcupine(accessKey, keywordIndex) {
47-
if (window.WebVoiceProcessor.WebVoiceProcessor.isRecording) {
48-
await window.WebVoiceProcessor.WebVoiceProcessor.unsubscribe(porcupine);
49-
await porcupine.terminate();
50-
}
51-
52-
writeMessage("Porcupine is loading. Please wait...");
53-
try {
54-
porcupine = await PorcupineWeb.PorcupineWorker.create(
55-
accessKey,
56-
[porcupineKeywords[keywordIndex]],
57-
porcupineKeywordCallback,
58-
porcupineModel
59-
);
60-
61-
writeMessage("Porcupine worker ready!");
62-
63-
writeMessage(
64-
"WebVoiceProcessor initializing. Microphone permissions requested ..."
65-
);
66-
await window.WebVoiceProcessor.WebVoiceProcessor.subscribe(porcupine);
67-
68-
writeMessage("WebVoiceProcessor ready and listening!");
69-
} catch (err) {
70-
porcupineErrorCallback(err);
71-
}
72-
}
73-
</script>
74-
</head>
75-
<body>
76-
<h1>Porcupine Web Demo</h1>
77-
<p>This demo uses Porcupine for Web and the WebVoiceProcessor to:</p>
78-
<ol>
79-
<li>
80-
Create an instance of Porcupine that listens for the selected keyword.
81-
</li>
82-
<li>
83-
Acquire microphone (& ask permission) data stream and convert to voice
84-
processing format (16kHz 16-bit linear PCM). The down-sampled audio is
85-
forwarded to the Porcupine engine. The audio <i>does not</i> leave the
86-
browser: all processing is occurring via the Porcupine WebAssembly code.
87-
</li>
88-
<li>
89-
Listen for keyword detection events from the Porcupine engines and output them to the page.
90-
</li>
91-
</ol>
92-
<p>After entering the AccessKey, click the "Start Porcupine" button.</p>
93-
<p>While listening you can try out different keywords using the "Keywords" dropdown.</p>
94-
<hr/>
95-
<label for="accessKey"
96-
>AccessKey obtained from
97-
<a href="https://console.picovoice.ai/">Picovoice Console</a>:</label
98-
>
99-
<input type="text" id="accessKey" name="accessKey"/>
100-
<input
101-
type="button"
102-
id="submit"
103-
value="Start Porcupine"
104-
onclick="startPorcupine(
3+
<head>
4+
<script src="node_modules/@picovoice/web-voice-processor/dist/iife/index.js"></script>
5+
<script src="node_modules/@picovoice/porcupine-web/dist/iife/index.js"></script>
6+
<script src="keywords/porcupineKeywords.js"></script>
7+
<script src="models/porcupineModel.js"></script>
8+
<script type="application/javascript" src="scripts/porcupine.js"></script>
9+
</head>
10+
<body>
11+
<h1>Porcupine Web Demo</h1>
12+
<p>This demo uses Porcupine for Web and the WebVoiceProcessor to:</p>
13+
<ol>
14+
<li>
15+
Create an instance of Porcupine that listens for the selected keyword.
16+
</li>
17+
<li>
18+
Acquire microphone (& ask permission) data stream and convert to voice
19+
processing format (16kHz 16-bit linear PCM). The down-sampled audio is
20+
forwarded to the Porcupine engine. The audio <i>does not</i> leave the
21+
browser: all processing is occurring via the Porcupine WebAssembly code.
22+
</li>
23+
<li>
24+
Listen for keyword detection events from the Porcupine engines and
25+
output them to the page.
26+
</li>
27+
</ol>
28+
<p>After entering the AccessKey, click the "Start Porcupine" button.</p>
29+
<p>
30+
While listening you can try out different keywords using the "Keywords"
31+
dropdown.
32+
</p>
33+
<hr />
34+
<label for="accessKey"
35+
>AccessKey obtained from
36+
<a href="https://console.picovoice.ai/">Picovoice Console</a>:</label
37+
>
38+
<input type="text" id="accessKey" name="accessKey" />
39+
<input
40+
type="button"
41+
id="submit"
42+
value="Start Porcupine"
43+
onclick="startPorcupine(
10544
document.getElementById('accessKey').value,
10645
parseInt(document.getElementById('keywords').value))"
107-
/>
108-
<br>
109-
<br>
110-
<label for="keywords">Keyword:</label>
111-
<select
112-
id="keywords"
113-
name="keywords"
114-
onchange="if (window.WebVoiceProcessor.WebVoiceProcessor.isRecording) { startPorcupine(
46+
/>
47+
<br />
48+
<br />
49+
<label for="keywords">Keyword:</label>
50+
<select
51+
id="keywords"
52+
name="keywords"
53+
onchange="if (window.WebVoiceProcessor.WebVoiceProcessor.isRecording) { startPorcupine(
11554
document.getElementById('accessKey').value,
116-
parseInt(document.getElementById('keywords').value)) }"></select>
117-
<hr/>
118-
<div id="status" style="white-space: pre;"></div>
119-
<br>
120-
<div id="result"></div>
121-
</body>
55+
parseInt(document.getElementById('keywords').value)) }"
56+
></select>
57+
<hr />
58+
<div id="status" style="white-space: pre"></div>
59+
<br />
60+
<div id="result"></div>
61+
</body>
12262
</html>

demo/web/package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
"@picovoice/web-voice-processor": "~4.0.8"
2222
},
2323
"devDependencies": {
24-
"http-server": "^14.0.0"
24+
"@eslint/js": "^9.22.0",
25+
"eslint": "^9.22.0",
26+
"globals": "^16.0.0",
27+
"http-server": "^14.0.0",
28+
"prettier": "3.5.1"
2529
}
2630
}

demo/web/scripts/porcupine.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
let porcupine = null;
2+
3+
window.addEventListener("load", function () {
4+
let usingBuiltIns = false;
5+
if (
6+
porcupineKeywords.length === 0 &&
7+
porcupineModel.publicPath.endsWith("porcupine_params.pv")
8+
) {
9+
usingBuiltIns = true;
10+
for (const k in PorcupineWeb.BuiltInKeyword) {
11+
porcupineKeywords.push(k);
12+
}
13+
}
14+
15+
let select = document.getElementById("keywords");
16+
for (let i = 0; i < porcupineKeywords.length; i++) {
17+
let el = document.createElement("option");
18+
el.textContent = usingBuiltIns
19+
? PorcupineWeb.BuiltInKeyword[porcupineKeywords[i]]
20+
: porcupineKeywords[i].label;
21+
el.value = `${i}`;
22+
select.appendChild(el);
23+
}
24+
});
25+
26+
function writeMessage(message) {
27+
console.log(message);
28+
document.getElementById("status").innerHTML = message;
29+
}
30+
31+
function porcupineErrorCallback(error) {
32+
writeMessage(error);
33+
}
34+
35+
function porcupineKeywordCallback(detection) {
36+
const time = new Date();
37+
const message = `keyword detected at ${time.toLocaleTimeString()}: ${detection.label} (index = ${detection.index})`;
38+
console.log(message);
39+
document.getElementById("result").innerHTML = message;
40+
}
41+
42+
async function startPorcupine(accessKey, keywordIndex) {
43+
if (window.WebVoiceProcessor.WebVoiceProcessor.isRecording) {
44+
await window.WebVoiceProcessor.WebVoiceProcessor.unsubscribe(porcupine);
45+
await porcupine.terminate();
46+
}
47+
48+
writeMessage("Porcupine is loading. Please wait...");
49+
try {
50+
porcupine = await PorcupineWeb.PorcupineWorker.create(
51+
accessKey,
52+
[porcupineKeywords[keywordIndex]],
53+
porcupineKeywordCallback,
54+
porcupineModel,
55+
);
56+
57+
writeMessage("Porcupine worker ready!");
58+
59+
writeMessage(
60+
"WebVoiceProcessor initializing. Microphone permissions requested ...",
61+
);
62+
await window.WebVoiceProcessor.WebVoiceProcessor.subscribe(porcupine);
63+
64+
writeMessage("WebVoiceProcessor ready and listening!");
65+
} catch (err) {
66+
porcupineErrorCallback(err);
67+
}
68+
}

demo/web/scripts/run_demo.js

+13-13
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,25 @@ const fs = require("fs");
33
const path = require("path");
44
const testData = require("../../../resources/.test/test_data.json");
55

6-
availableLanguages = testData["tests"]["singleKeyword"].map(
7-
(x) => x["language"]
6+
const availableLanguages = testData["tests"]["singleKeyword"].map(
7+
(x) => x["language"],
88
);
99

1010
const language = process.argv.slice(2)[0];
1111
if (!language) {
1212
console.error(
1313
`Choose the language you would like to run the demo in with "yarn start [language]".\nAvailable languages are ${availableLanguages.join(
14-
", "
15-
)}`
14+
", ",
15+
)}`,
1616
);
1717
process.exit(1);
1818
}
1919

2020
if (!availableLanguages.includes(language)) {
2121
console.error(
2222
`'${language}' is not an available demo language.\nAvailable languages are ${availableLanguages.join(
23-
", "
24-
)}`
23+
", ",
24+
)}`,
2525
);
2626
process.exit(1);
2727
}
@@ -34,7 +34,7 @@ const keywordDir = path.join(
3434
rootDir,
3535
"resources",
3636
`keyword_files${suffix}`,
37-
"wasm"
37+
"wasm",
3838
);
3939

4040
let outputDirectory = path.join(__dirname, "..", "keywords");
@@ -77,7 +77,7 @@ ${keywordJS.join("\n")}
7777
(function () {
7878
if (typeof module !== "undefined" && typeof module.exports !== "undefined")
7979
module.exports = porcupineKeywords;
80-
})();`
80+
})();`,
8181
);
8282

8383
const modelDir = path.join(rootDir, "lib", "common");
@@ -94,7 +94,7 @@ if (fs.existsSync(outputDirectory)) {
9494
const modelName = `porcupine_params${suffix}.pv`;
9595
fs.copyFileSync(
9696
path.join(modelDir, modelName),
97-
path.join(outputDirectory, modelName)
97+
path.join(outputDirectory, modelName),
9898
);
9999

100100
fs.writeFileSync(
@@ -107,12 +107,12 @@ fs.writeFileSync(
107107
(function () {
108108
if (typeof module !== "undefined" && typeof module.exports !== "undefined")
109109
module.exports = porcupineModel;
110-
})();`
110+
})();`,
111111
);
112112

113-
const command = (process.platform === "win32") ? "npx.cmd" : "npx";
113+
const command = process.platform === "win32" ? "npx.cmd" : "npx";
114114

115-
child_process.execSync(`${command} http-server -a localhost -p 5000`, {
115+
child_process.execSync(`${command} http-server -a localhost -p 5000`, {
116116
shell: true,
117-
stdio: 'inherit'
117+
stdio: "inherit",
118118
});

0 commit comments

Comments
 (0)