LipSurf Documentation
website·Indexed 33 days ago
https://docs.lipsurf.com/Developer documentation for LipSurf, an extensible voice-control system. Includes guides on creating plugins by extending PluginBase, managing commands and contexts, using the Custom STT API v1.0.0, and implementing standalone functionality via LipSurf.js.
What's inside LipSurf
- LipSurf is a Google Chrome extension that allows users to browse the web using voice commands, serving as a hands-free alternative or augmentation to the mouse and keyboard. It features an extensible architecture where developers can create simple, declarative plugins to automate browser actions, support over 100 languages, and utilize command chaining.
Override LipSurf built-in STT with a custom API
The Custom STT API (v1.0.0) allows LipSurf Enterprise users to replace the default Speech-to-Text engine with an external provider. Communication with LipSurf is handled viawindow.postMessageusing a specific object structure containingforLipSurf: true, amethodname, and anargsarray.Manage command availability using Contexts
Contexts in LipSurf are groups of commands used to determine which commands are valid based on the page's current state. LipSurf can be in one or more contexts simultaneously, and contexts are tracked per-tab.
By default, commands without a specified context belong to the
PluginBase.constants.contexts.Normalcontext.Key behaviors:
- Context Order: If multiple contexts define the same command (e.g., two 'search *' commands), the command from the context appearing earlier in the context list takes precedence.
- Context Creation: Contexts are defined via a command's
enterContextproperty or a plugin'scontextsproperty. - Cleanup: Any plugin-specific contexts must be removed in the plugin's
destroyfunction to prevent leaks.
Develop LipSurf plugins and commands
LipSurf is extensible via plugins. A plugin is a group of commands, where each command performs a specific action based on voice input. All commands must be part of a plugin.Understand LipSurf Plugin Architecture
LipSurf uses a plugin-based architecture to provide voice functionality. All LipSurf Commands must be members of a Plugin. Plugins can be site-specific (e.g., a plugin for Reddit) or functionality-specific (e.g., a plugin that modifies how tags are displayed). This separation allows the core extension to remain stable while plugins evolve alongside website updates. Plugins can be installed by default, added by users, or shared with the community.Create and install a basic LipSurf plugin
To create a LipSurf plugin, use the LipSurf CLI to scaffold a project, define the plugin logic in TypeScript, compile it, and load the resulting.lsfile into the LipSurf Chrome extension via Developer mode.# 1. Install the LipSurf CLI yarn global add @lipsurf/cli # 2. Scaffold a project lipsurf-cli init HelloWorld && cd lipsurf-plugin-helloworld # 3. Compile TypeScript files and watch for changes yarn watchCapture arbitrary arguments using wildcard matching
Use the asterisk*in a command'smatchstring to greedily match any words. This acts as a 'slot' that captures a variable part of the user's spoken command, which can then be processed in thepageFn.commands: [ { name: "Check the Weather", global: true, match: "[weather/forecast] [for/in] *", pageFn: async (transcript, { preTs, normTs }: TsData) => { // preTs will contain the text captured by the wildcard '*' const resp = await fetch(`https://api.accuweather.com/...q=${preTs}...`); // ... logic to redirect to weather page }, }, ]Implement cross-tab communication in a Lipsurf plugin
To send commands from a Lipsurf plugin to a specific tab or window, usechrome.tabs.queryto find the target tab andchrome.tabs.sendMessageto dispatch a message. On the receiving end, usechrome.runtime.onMessage.addListenerwithin the plugin'sinitfunction to listen for and act upon these messages. It is recommended to wrap the listener in a URL check (e.g., using a regex match onwindow.location.origin) to ensure the listener only activates on relevant pages.export default <IPluginBase & IPlugin>{ ...PluginBase, ...{ niceName: 'Spotify', init: function () { // Only attach the listener if the current page matches the target site if (spotifyPlayerUrlRegexMatch.test(window.location.origin)) { chrome.runtime.onMessage.addListener((msg) => { if (msg.type === 'lsSpotify') { switch (msg.control) { case SpotifyControlButtons.Play: { clickButton(SpotifyControlButtons.Play); break; } } } }); } }, commands: [ { name: 'spotify play', description: 'Play the Spotify web player.', global: true, match: 'spotify play', fn: function () { const control = SpotifyControlButtons.Play; return new Promise(resolve => { // Find the tab matching the target URL chrome.tabs.query({url: spotifyPlayerUrlMatch}, (tabs) => { const tab = tabs.length ? tabs[0] : null; if (tab) { // Send the message to the specific tab chrome.tabs.sendMessage(tab.id, {type: 'lsSpotify', control}, () => { resolve(); }); } else { resolve(); } }); }); } }, ] } }Execute logic in Page vs. Extension context
LipSurf provides two different execution contexts for commands:
pageFn: Runs in the context of the web page. It has access to the DOM but cannot access Chrome extension APIs (e.g.,chrome.tabs).fn: Runs in the sandboxed Chrome extension context. It has access to Chrome extension APIs but cannot access the page DOM.
Both are async functions. To support command chaining, you must resolve the promise when the execution is complete.
// Example of pageFn with a delay for chaining pageFn: async (transcript, ...matchOutput) => { await doSomethingInDom(); return await PluginBase.util.sleep(500); }Get help with LipSurf plugin development
For development questions, design choice clarifications, or help with specific use cases, post in the 'Plugin Development' category of the LipSurf Forum. LipSurf does not use chat support channels to ensure that Q&A remains searchable for the community.Forum Link: https://discuss.lipsurf.com/c/plugin-developmentCreate a LipSurf plugin using PluginBase
To create a LipSurf plugin, extend thePluginBaseclass. The plugin definition includes metadata such as a friendly name, version, and a set of commands. You can also define URL matching rules to determine where the plugin is active.class MyPlugin extends PluginBase { name = 'My Plugin'; version = '1.0.0'; match = /https:\ menjadikan\.com/; commands = [ /* Command definitions */ ]; }Use compact form syntax for command match variations
To avoid writing numerous individual match strings for a single command, use brackets
[]to define optional or alternative words. The syntax expands to all possible combinations of the words within the brackets.Constraints:
- Brackets cannot be nested.
- Priority is determined by the order of words within the brackets.
// This match string expands to: // 'delete previous word', 'remove previous word', 'delete the previous word', // 'remove the previous word', 'delete previous # words', etc. match: '[delete/remove] [/the ]previous [word/# words]'