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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ jobs:
- name: Install
run: npm ci

- name: No raw colors
run: npm run check:colors

- name: Test
run: npm test

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ prisma/*.db-journal
# Keep generated SFX (small, needed to mix)
# data/voices.json is committed as the default character map
.vercel
*.tsbuildinfo
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ Default routing (not intelligent): Indic languages → Sarvam `bulbul:v3`. Engli

## Stack (grows only when the audio works)

- Next.js App Router + TypeScript + Tailwind
- Next.js App Router + TypeScript + Tailwind for layout
- House design system (`@house/tokens`, `@house/react`, vendored in `vendor/`) for color, type, and controls; `npm run check:colors` fails on raw colors
- Local JSON + wavs on disk, then Prisma (SQLite locally; Postgres when you need it)
- FFmpeg via `ffmpeg-static` (system `ffmpeg` used if on PATH)
- Sarvam first. ElevenLabs TTS only, as a week-4 benchmark. No Music API.
Expand Down
76 changes: 39 additions & 37 deletions app/create-form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { useMemo, useState } from "react";
import { useId, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { Button, Field, Stack, Surface, Text } from "@house/react";
import { USE_CASES, type UseCaseId } from "@/lib/use-cases";
import { parseCanonicalScript } from "@/server/pipeline/parse";
import { useOptionalOnboarding } from "./onboarding/provider";
Expand All @@ -23,6 +24,7 @@ export function CreateEpisodeForm() {
const [script, setScript] = useState(selected.script);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const scriptId = useId();

const parsed = useMemo(() => tryParse(script), [script]);

Expand Down Expand Up @@ -58,7 +60,7 @@ export function CreateEpisodeForm() {
return (
<form onSubmit={onSubmit} className="mt-8 space-y-6">
<div>
<p className="text-sm uppercase tracking-widest text-[var(--muted)]">Use case</p>
<p className="text-sm uppercase tracking-widest text-[var(--text-muted)]">Use case</p>
<div data-tour="use-cases" className="mt-3 grid gap-3 sm:grid-cols-3">
{USE_CASES.map((useCase) => {
const on = useCase.id === active;
Expand All @@ -69,79 +71,79 @@ export function CreateEpisodeForm() {
onClick={() => pick(useCase.id)}
className={`rounded-lg border px-3 py-3 text-left ${
on
? "border-[var(--accent)] bg-[var(--panel)]"
: "border-[var(--line)] bg-transparent hover:border-[var(--accent)]"
? "border-[var(--accent)] bg-[var(--accent-subtle)]"
: "border-[var(--border)] bg-transparent hover:border-[var(--accent)]"
}`}
aria-pressed={on}
>
<p className="text-sm">{useCase.title}</p>
<p className="mt-1 text-xs text-[var(--muted)]">{useCase.who}</p>
<p className="mt-2 text-xs leading-relaxed text-[var(--muted)]">{useCase.blurb}</p>
<p className="mt-1 text-xs text-[var(--text-muted)]">{useCase.who}</p>
<p className="mt-2 text-xs leading-relaxed text-[var(--text-muted)]">
{useCase.blurb}
</p>
</button>
);
})}
</div>
</div>

<div className="grid gap-6 lg:grid-cols-[1fr_16rem]">
<div className="space-y-4">
<label className="block text-sm text-[var(--muted)]">
Title
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
className="mt-1 w-full rounded-md border border-[var(--line)] bg-[var(--panel)] px-3 py-2 text-[var(--text)]"
/>
</label>
<label className="block text-sm text-[var(--muted)]">
Script
<Stack gap={4}>
<Field label="Title" value={title} onChange={(e) => setTitle(e.target.value)} />
<Stack gap={1}>
<Text as="label" size="sm" weight="medium" htmlFor={scriptId}>
Script
</Text>
<textarea
id={scriptId}
value={script}
onChange={(e) => setScript(e.target.value)}
rows={16}
className="mt-1 w-full rounded-md border border-[var(--line)] bg-[var(--panel)] px-3 py-2 font-mono text-sm text-[var(--text)]"
className="w-full rounded-[var(--radius-md)] border border-[var(--border-strong)] bg-[var(--surface)] px-3 py-2 font-mono text-sm text-[var(--text)]"
/>
</label>
</div>
</Stack>
</Stack>

<aside
data-tour="parse-panel"
className="h-fit rounded-lg border border-[var(--line)] bg-[var(--panel)] p-4"
>
<p className="mono text-xs uppercase tracking-widest text-[var(--muted)]">Parse</p>
<Surface as="aside" data-tour="parse-panel" className="h-fit">
<p className="mono text-xs uppercase tracking-widest text-[var(--text-muted)]">Parse</p>
{parsed ? (
<>
<p className="mt-3 text-2xl">{parsed.plan.lines.length}</p>
<p className="text-xs text-[var(--muted)]">lines · {parsed.plan.sfx.length} SFX</p>
<p className="text-xs text-[var(--text-muted)]">
lines · {parsed.plan.sfx.length} SFX
</p>
<div className="mt-4 space-y-2">
{parsed.plan.characters.map((character) => (
<p key={character.name} className="text-sm">
{character.name}
<span className="mono ml-2 text-xs text-[var(--muted)]">{character.role}</span>
<span className="mono ml-2 text-xs text-[var(--text-muted)]">
{character.role}
</span>
</p>
))}
</div>
{parsed.plan.sfx.length > 0 && (
<p className="mono mt-4 text-xs text-[var(--muted)]">
<p className="mono mt-4 text-xs text-[var(--text-muted)]">
{parsed.plan.sfx.map((s) => s.tag).join(" · ")}
</p>
)}
</>
) : (
<p className="mt-3 text-sm text-[var(--muted)]">
<p className="mt-3 text-sm text-[var(--text-muted)]">
Add [CHARACTER] headers to see the plan.
</p>
)}
</aside>
</Surface>
</div>

{error && <p className="text-[var(--bad)]">{error}</p>}
<button
type="submit"
disabled={busy || !parsed}
className="rounded-md bg-[var(--accent)] px-4 py-2 text-black disabled:opacity-50"
>
{error && (
<Text as="p" status="danger">
{error}
</Text>
)}
<Button type="submit" variant="primary" disabled={busy || !parsed}>
{busy ? "Creating…" : "Open review desk"}
</button>
</Button>
</form>
);
}
2 changes: 1 addition & 1 deletion app/demo-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function OpenDeskLink({ href, children }: { href: string; children: React
<Link
href={href}
data-tour="open-desk"
className="rounded-md bg-[var(--accent)] px-3 py-2 text-sm text-black"
className="rounded-[var(--radius-sm)] bg-[var(--accent)] px-4 py-2 text-sm font-medium text-[var(--accent-contrast)]"
onClick={() => onboarding?.completeAction("open-desk")}
>
{children}
Expand Down
16 changes: 5 additions & 11 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
@import "tailwindcss";

/* Colors come from @house/tokens. AudioForge keeps its editorial serif. */
:root {
--bg: #0f1115;
--panel: #171a21;
--line: #2a2f3a;
--text: #ece8df;
--muted: #9a9386;
--ok: #7dcea0;
--warn: #e6b566;
--bad: #e07a6b;
--accent: #d4a574;
--font-sans: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
--scrim: color-mix(in srgb, var(--bg) 72%, transparent);
}

html,
body {
background: var(--bg);
color: var(--text);
font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
font-family: var(--font-sans);
}

body {
Expand All @@ -29,5 +23,5 @@ button {

code,
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-family: var(--font-mono);
}
4 changes: 3 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Metadata } from "next";
import { OnboardingShell } from "./onboarding/shell";
import { WebVitals } from "./web-vitals";
import "@house/tokens/theme.css";
import "@house/react/styles.css";
import "./globals.css";

export const metadata: Metadata = {
Expand All @@ -10,7 +12,7 @@ export const metadata: Metadata = {

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<html lang="en" data-theme="dark">
<body className="antialiased">
<OnboardingShell>{children}</OnboardingShell>
<WebVitals />
Expand Down
2 changes: 1 addition & 1 deletion app/onboarding/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { formatKn } from "./steps";

export function StepBadge({ index, total }: { index: number; total: number }) {
return (
<span className="mono rounded-full border border-[var(--accent)] bg-[var(--bg)] px-2 py-0.5 text-xs text-[var(--accent)]">
<span className="mono rounded-full border border-[var(--accent)] bg-[var(--accent-subtle)] px-2 py-0.5 text-xs text-[var(--accent)]">
{formatKn(index, total)}
</span>
);
Expand Down
23 changes: 8 additions & 15 deletions app/onboarding/banner.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { Button } from "@house/react";
import { StepBadge } from "./badge";
import { useOnboarding } from "./provider";

Expand All @@ -8,31 +9,23 @@ export function OnboardingBanner() {
if (completed || paused || !step || step.kind !== "tour") return null;

return (
<div className="sticky top-0 z-30 border-b border-[var(--line)] bg-[var(--panel)]">
<div className="sticky top-0 z-30 border-b border-[var(--border)] bg-[var(--surface)]">
<div className="mx-auto flex max-w-5xl flex-wrap items-center gap-3 px-6 py-3">
<StepBadge index={index} total={total} />
<p className="min-w-0 flex-1 text-sm">
<span className="text-[var(--text)]">{step.title}. </span>
<span className="text-[var(--muted)]">{step.body}</span>
<span className="text-[var(--text-muted)]">{step.body}</span>
</p>
<div className="flex flex-wrap items-center gap-2">
{index > 1 && (
<button
type="button"
onClick={back}
className="rounded border border-[var(--line)] px-2 py-1 text-sm"
>
<Button size="sm" onClick={back}>
Back
</button>
</Button>
)}
<button
type="button"
onClick={next}
className="rounded-md bg-[var(--accent)] px-3 py-1 text-sm text-black"
>
<Button size="sm" variant="primary" onClick={next}>
Next
</button>
<button type="button" onClick={skip} className="text-sm text-[var(--muted)]">
</Button>
<button type="button" onClick={skip} className="text-sm text-[var(--text-muted)]">
Skip
</button>
</div>
Expand Down
28 changes: 11 additions & 17 deletions app/onboarding/coachmark.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useEffect, useState } from "react";
import { Button, Surface } from "@house/react";
import { StepBadge } from "./badge";
import { useOnboarding } from "./provider";

Expand Down Expand Up @@ -59,7 +60,7 @@ export function OnboardingCoachmark() {
left: hole.left,
width: hole.width,
height: hole.height,
boxShadow: "0 0 0 9999px rgba(0,0,0,0.55)",
boxShadow: "0 0 0 9999px var(--scrim)",
}}
/>
<div
Expand All @@ -68,8 +69,9 @@ export function OnboardingCoachmark() {
>
<StepBadge index={index} total={total} />
</div>
<div
className="pointer-events-auto absolute w-[min(20rem,calc(100vw-2rem))] rounded-lg border border-[var(--line)] bg-[var(--panel)] p-4"
<Surface
raised
className="pointer-events-auto absolute w-[min(20rem,calc(100vw-2rem))]"
style={{
top: tipTop,
left: Math.min(hole.left, window.innerWidth - 336),
Expand All @@ -79,7 +81,7 @@ export function OnboardingCoachmark() {
<StepBadge index={index} total={total} />
</div>
<p className="mt-2 text-sm">{step.title}</p>
<p className="mt-1 text-sm text-[var(--muted)]">{step.body}</p>
<p className="mt-1 text-sm text-[var(--text-muted)]">{step.body}</p>
{step.cheat && (
<ul className="mono mt-3 space-y-1 text-xs text-[var(--accent)]">
{step.cheat.map((line) => (
Expand All @@ -89,23 +91,15 @@ export function OnboardingCoachmark() {
)}
<div className="mt-3 flex gap-2">
{index > 1 && (
<button
type="button"
onClick={back}
className="rounded border border-[var(--line)] px-2 py-1 text-sm"
>
<Button size="sm" onClick={back}>
Back
</button>
</Button>
)}
<button
type="button"
onClick={next}
className="rounded-md bg-[var(--accent)] px-3 py-1 text-sm text-black"
>
<Button size="sm" variant="primary" onClick={next}>
Next
</button>
</Button>
</div>
</div>
</Surface>
</div>
);
}
Loading
Loading