diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..67caa57 --- /dev/null +++ b/vercel.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "outputDirectory": "website" +} diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..6f9d58c --- /dev/null +++ b/website/index.html @@ -0,0 +1,64 @@ + + + + + + + + + + + Faster conversational voice agents, built in the open - Premove + + + + + +
+

Premove

+

Faster conversational voice agents, built in the open

+ +

I got obsessed with exploring voice agents after making the same frustrating phone call over and over again. I thought making the agent smart would be the hard part. But I kept running into the less glamorous problems underneath it: latency, transcription errors, tool calls, and all the small things that make a conversation feel off.

+ +

Premove is my effort to work on those problems in the open, one by one.

+ +

The broader project is still under development. As parts become useful on their own, I’ll release them independently instead of waiting for the whole thing to be finished.

+ +

Premove ITN: Context-aware inverse text normalization

+ +

The first released piece is Premove ITN, an open-source, context-aware inverse text normalizer for English voice-agent transcripts.

+ +

One of the first problems I hit was that an ASR transcript could look completely fine to me and still be wrong for the tool behind the agent.

+ +
+ +
my room number is two thirty
+→ room_number = 230
+
+meet me at two thirty
+→ 02:30
+
+ +

If a normalizer always rewrites “two thirty” as 02:30, it sends the wrong value for the room-number request:

+ +
+ +
my room number is two thirty
+→ room_number = 02:30  (wrong; the tool expects 230)
+
+ +

The same spoken words can need different written forms depending on context.

+ +

That is why I built Premove ITN: to turn spoken values like IDs, phone numbers, dates, times, amounts, and emails into the structured forms downstream tools expect.

+ +

Explore Premove ITN →

+
+ + + diff --git a/website/premove-icon.png b/website/premove-icon.png new file mode 100644 index 0000000..92c171b Binary files /dev/null and b/website/premove-icon.png differ diff --git a/website/site.js b/website/site.js new file mode 100644 index 0000000..bf3275b --- /dev/null +++ b/website/site.js @@ -0,0 +1,31 @@ +const root = document.documentElement; +const themeToggle = document.querySelector(".theme-toggle"); +const savedTheme = localStorage.getItem("premove-theme"); + +if (savedTheme === "light" || savedTheme === "dark") root.dataset.theme = savedTheme; + +function updateThemeLabel() { + const isDark = root.dataset.theme + ? root.dataset.theme === "dark" + : matchMedia("(prefers-color-scheme: dark)").matches; + themeToggle.setAttribute("aria-label", `Switch to ${isDark ? "light" : "dark"} theme`); +} + +themeToggle.addEventListener("click", () => { + root.dataset.theme = themeToggle.getAttribute("aria-label") === "Switch to light theme" + ? "light" : "dark"; + localStorage.setItem("premove-theme", root.dataset.theme); + updateThemeLabel(); +}); + +matchMedia("(prefers-color-scheme: dark)").addEventListener("change", updateThemeLabel); +updateThemeLabel(); + +document.querySelectorAll(".copy-button").forEach((button) => { + button.addEventListener("click", async () => { + const code = button.parentElement.querySelector("code").textContent; + await navigator.clipboard.writeText(code); + button.setAttribute("aria-label", "Copied"); + setTimeout(() => button.setAttribute("aria-label", "Copy the contents from the code block"), 2000); + }); +}); diff --git a/website/style.css b/website/style.css new file mode 100644 index 0000000..7edacbc --- /dev/null +++ b/website/style.css @@ -0,0 +1,177 @@ +:root { + color-scheme: light dark; + font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-synthesis: none; + --background: #fff; + --foreground: #111; + --muted: #4b5563; + --border: #e5e7eb; + --code-background: #f9fafb; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0b0b0d; + --foreground: #f9fafb; + --muted: #d1d5db; + --border: #202024; + --code-background: #0d0d0f; + } +} + +:root[data-theme="light"] { + color-scheme: light; + --background: #fff; + --foreground: #111; + --muted: #4b5563; + --border: #e5e7eb; + --code-background: #f9fafb; +} + +:root[data-theme="dark"] { + color-scheme: dark; + --background: #0b0b0d; + --foreground: #f9fafb; + --muted: #d1d5db; + --border: #202024; + --code-background: #0d0d0f; +} + +* { box-sizing: border-box; } + +html { background: var(--background); } + +body { + margin: 0; + min-width: 320px; + color: var(--foreground); + background: var(--background); +} + +a { color: inherit; } + +.skip-link { + position: absolute; + left: 1rem; + top: -4rem; + z-index: 2; + padding: .5rem; + background: var(--background); +} +.skip-link:focus { top: .5rem; } + +.site-header { + display: flex; + align-items: center; + justify-content: space-between; + height: 64px; + padding: 0 48px; + border-bottom: 1px solid var(--border); +} + +.brand { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 1.25rem; + font-weight: 700; + text-decoration: none; +} + +.brand img { filter: brightness(0); } +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .brand img { filter: brightness(0) invert(1); } +} +:root[data-theme="dark"] .brand img { filter: brightness(0) invert(1); } + +.theme-toggle { + width: 32px; + height: 32px; + border: 0; + color: var(--foreground); + background: transparent; + cursor: pointer; + font-size: 1.15rem; +} +.theme-toggle::before { content: "☾"; } +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .theme-toggle::before { content: "☀"; } +} +:root[data-theme="dark"] .theme-toggle::before { content: "☀"; } + +main { + max-width: 752px; + margin: 48px auto 64px; + padding: 0 24px; +} + +.eyebrow { + margin: 0 0 8px; + font-size: .875rem; + font-weight: 600; + color: var(--foreground); +} + +h1 { + margin: 0 0 24px; + font-size: 2.5rem; + font-weight: 700; + letter-spacing: -.025em; + line-height: 1.15; +} + +h2 { + margin: 32px 0 16px; + font-size: 1.5rem; + font-weight: 600; + line-height: 1.3; +} + +main p { + margin: 0 0 16px; + color: var(--muted); + font-size: 1rem; + line-height: 1.7; +} + +main p a { color: var(--foreground); font-weight: 600; } +main p strong { color: var(--foreground); } + +.code-block { + position: relative; + margin: 20px 0; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--code-background); +} + +pre { + margin: 0; + padding: 18px 20px; + overflow-x: auto; + font-size: .875rem; + line-height: 1.6; +} + +code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } + +.copy-button { + position: absolute; + top: 10px; + right: 10px; + padding: 4px; + border: 0; + color: var(--muted); + background: var(--code-background); + cursor: pointer; + font-size: 1rem; +} + +.explore-link { display: inline-block; margin-top: 4px; } + +@media (max-width: 640px) { + .site-header { height: 64px; padding: 0 16px; } + main { margin-top: 40px; padding: 0 16px; } + h1 { font-size: 2.5rem; } + h2 { font-size: 1.5rem; } +}