npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hanzo/voice

v0.1.8

Published

Bidirectional voice for a composer — open mic, live transcript, spoken replies, barge-in. One machine and one button, shared by hanzo.chat and hanzo.app.

Readme

@hanzo/voice

Bidirectional voice for a composer. One machine, one button, shared by hanzo.chat and hanzo.app.

Click the mic and the microphone stays open: the transcript streams into the composer as it is heard, each pause sends a turn through the composer's own submit path, and the reply is read back aloud. Speak over the reply and it stops so you can take the next turn. Click again to end the conversation.

npm i @hanzo/voice

Use

The caller keeps the machine — it needs say to read the reply — and the button only draws it.

import { Voice, useVoice, speech } from "@hanzo/voice";

const platform = speech({ token: () => iam.getValidAccessToken() });

function Composer() {
  const [text, setText] = useState("");
  const voice = useVoice({
    speech: platform,
    onPartial: setText,                 // heard so far — show it
    onUtterance: (said) => submit(said), // the EXISTING submit path
  });

  // Read the reply back, but only inside a conversation.
  useEffect(() => { if (done) void voice.say(reply); }, [done]);

  return (
    <form onSubmit={...}>
      <textarea value={text} onChange={...} />
      <Voice voice={voice} className="my-composer-icon" />
    </form>
  );
}

onUtterance is deliberately the composer's existing submit. Voice is a way of typing, never a second way of sending.

A conversation belongs to the page, not to the component drawing it. Sending the first turn is exactly what makes a chat surface swap /c/new for /c/<id>, remounting the composer — so the machine picks the microphone back up on the other side. Only a click ends a conversation. A fresh page load always starts closed.

What it uses

The platform's own speech services — POST /v1/audio/transcriptions and POST /v1/audio/speech on the Hanzo gateway, both OpenAI-compatible and metered. speech() builds that transport; a surface that fronts those paths with its own proxy passes its own baseUrl and nothing else changes.

Hanzo's own transcriber listens wherever it is configured and the browser can record for it — every browser, not only the ones missing a recogniser. The browser's own recogniser is the standby:

| | Listens with | Live partials | | --- | --- | --- | | speech supplied (the default) | Hanzo's /v1/audio/transcriptions | the phrase arrives whole | | no speech, or prefer: "browser" | the built-in recogniser | yes, word by word | | Hanzo's transcriber refused | the built-in recogniser, from that moment | yes, word by word |

Replies are read by the Hanzo voice when speech.speak is supplied, and by the browser's own voice when it is not — and "is not" includes "refused".

Both legs hold an echo-cancelled capture stream for as long as they are open. Without it the microphone hears the reply coming out of the speakers and interrupts itself on every turn.

When it refuses

A refused service is never swallowed. A 401, an empty balance or a timeout puts the browser behind the microphone or behind the reply, and puts a refusal on the machine at the same moment:

voice.refusal // { service: "ear" | "mouth", error, covered } | null

covered is whether the browser could stand in. <Voice/> wears the sentence in its label and marks itself data-refusal; a surface drawing its own button reads voice.refusal or takes onRefusal. Standing in quietly would make a dead key sound exactly like a live one, and nobody goes looking for a bill that reads zero.

When it can't

voice.blocked is one of insecure, unsupported, denied, absent, and voice.reason is a sentence a person can act on. The button stays put, disabled, wearing that reason — the composer still types. A control that quietly disappears teaches the user nothing.

API

  • useVoice({ onUtterance, onPartial?, onLevel?, speech?, prefer?, onRefusal?, voice?, language?, pause? }) → the machine: state (idle | listening | speaking), open, blocked, reason, refusal, toggle(), say(text), hush(). open is the user's intent, true from the click; state is what is actually happening, and it does not say listening until the microphone really is. The button reads aria-pressed off the first and data-state off the second.
  • <Voice voice={...} className? disabled? children? /> — the button. Unstyled by design; supply the chrome.
  • speech({ baseUrl?, token?, ear?, voice?, fetch? }) — the platform transport.
  • listen, mouth, capability, blocker — the pieces, if you want them.

Apache-2.0 · hanzo.ai