Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"outputDirectory": "website"
}
64 changes: 64 additions & 0 deletions website/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Open-source work to reduce end-to-end latency in conversational voice-agent systems." />
<meta name="theme-color" content="#0b0b0d" />
<link rel="canonical" href="https://premove.dev/" />
<link rel="icon" href="/premove-icon.png" type="image/png" />
<link rel="stylesheet" href="/style.css" />
<title>Faster conversational voice agents, built in the open - Premove</title>
</head>
<body>
<a class="skip-link" href="#content">Skip to main content</a>
<header class="site-header">
<a class="brand" href="/" aria-label="Premove home page">
<img src="/premove-icon.png" alt="" width="24" height="24" />
<span>Premove</span>
</a>
<button class="theme-toggle" type="button" aria-label="Switch to light theme" title="Switch theme"></button>
</header>

<main id="content">
<p class="eyebrow">Premove</p>
<h1>Faster conversational voice agents, built in the open</h1>

<p>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.</p>

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

<p>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.</p>

<h2>Premove ITN: Context-aware inverse text normalization</h2>

<p>The first released piece is <a href="https://docs.premove.dev/itn/">Premove ITN</a>, an open-source, context-aware inverse text normalizer for English voice-agent transcripts.</p>

<p>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.</p>

<div class="code-block">
<button class="copy-button" type="button" aria-label="Copy the contents from the code block" title="Copy code">⧉</button>
<pre><code>my room number is two thirty
→ room_number = 230

meet me at two thirty
→ 02:30</code></pre>
</div>

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

<div class="code-block">
<button class="copy-button" type="button" aria-label="Copy the contents from the code block" title="Copy code">⧉</button>
<pre><code>my room number is two thirty
→ room_number = 02:30 (wrong; the tool expects 230)</code></pre>
</div>

<p>The same spoken words can need different written forms depending on context.</p>

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

<p><a class="explore-link" href="https://docs.premove.dev/itn/">Explore Premove ITN →</a></p>
</main>
<script src="/site.js" defer></script>
</body>
</html>
Binary file added website/premove-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions website/site.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
177 changes: 177 additions & 0 deletions website/style.css
Original file line number Diff line number Diff line change
@@ -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; }
}
Loading