GitHub Actions commited on
Commit
00954e2
·
0 Parent(s):

Deploy docs from github.com/TTS-AGI/TTS-Arena@d7c27b1

Browse files
Dockerfile ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+ #
3
+ # Hugging Face Space image for the docs site (Fumadocs/Next.js). The docs app is
4
+ # fully self-contained — it has no @ttsa/* workspace dependencies — so we build
5
+ # it in isolation, NOT as part of the monorepo workspace. That avoids pulling in
6
+ # unrelated packages (e.g. web's native better-sqlite3) that would need a build
7
+ # toolchain and have nothing to do with the docs.
8
+ FROM oven/bun:1.1.42-debian AS base
9
+ WORKDIR /app/docs
10
+
11
+ # ── deps ──
12
+ FROM base AS deps
13
+ # Standalone install: only the docs package.json (no root workspace context).
14
+ COPY apps/docs/package.json ./package.json
15
+ RUN bun install
16
+
17
+ # ── build ──
18
+ FROM base AS build
19
+ COPY --from=deps /app/docs/node_modules ./node_modules
20
+ COPY apps/docs/ ./
21
+ RUN bun run build
22
+ # Stage a flat runtime dir. In this isolated build (no parent workspace) Next's
23
+ # standalone output is flat — server.js, node_modules and package.json at the
24
+ # standalone root — but we locate server.js explicitly so the image is correct
25
+ # regardless of any workspace-root inference, then add the static + public dirs
26
+ # the standalone server expects beside it.
27
+ RUN set -e; \
28
+ SA="$PWD/.next/standalone"; \
29
+ SERVER="$(find "$SA" -name server.js -not -path '*/node_modules/*' | head -1)"; \
30
+ test -n "$SERVER"; \
31
+ ROOT="$(dirname "$SERVER")"; \
32
+ mkdir -p "$ROOT/.next"; \
33
+ cp -a "$PWD/.next/static" "$ROOT/.next/static"; \
34
+ [ -d "$PWD/public" ] && cp -a "$PWD/public" "$ROOT/public" || true; \
35
+ cp -a "$ROOT" /out
36
+
37
+ # ── runtime ──
38
+ FROM base AS runtime
39
+ ENV NODE_ENV=production
40
+ ENV PORT=7860
41
+ ENV HOSTNAME=0.0.0.0
42
+ EXPOSE 7860
43
+
44
+ WORKDIR /app
45
+ COPY --from=build /out/ ./
46
+ CMD ["bun", "server.js"]
README.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: TTS Arena Docs
3
+ emoji: 📚
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # TTS Arena Docs
12
+
13
+ Documentation for [TTS Arena](https://huggingface.co/spaces/TTS-AGI/TTS-Arena-V2),
14
+ served at https://docs.ttsarena.org/.
15
+
16
+ > Source: https://github.com/TTS-AGI/TTS-Arena (apps/docs)
17
+ >
18
+ > Built with Fumadocs + Next.js. This Space is deployed automatically from the
19
+ > GitHub repo; do not edit it directly.
apps/docs/.gitignore ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # deps
2
+ /node_modules
3
+
4
+ # generated content
5
+ .source
6
+
7
+ # test & build
8
+ /coverage
9
+ /.next/
10
+ /out/
11
+ /build
12
+ *.tsbuildinfo
13
+
14
+ # misc
15
+ .DS_Store
16
+ *.pem
17
+ /.pnp
18
+ .pnp.js
19
+ npm-debug.log*
20
+ yarn-debug.log*
21
+ yarn-error.log*
22
+
23
+ # others
24
+ .env*.local
25
+ .vercel
26
+ next-env.d.ts
apps/docs/app/[[...slug]]/page.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getPageImage, getPageMarkdownUrl, source } from "@/lib/source";
2
+ import {
3
+ DocsBody,
4
+ DocsDescription,
5
+ DocsPage,
6
+ DocsTitle,
7
+ MarkdownCopyButton,
8
+ ViewOptionsPopover,
9
+ } from "fumadocs-ui/layouts/docs/page";
10
+ import { notFound } from "next/navigation";
11
+ import { getMDXComponents } from "@/components/mdx";
12
+ import type { Metadata } from "next";
13
+ import { createRelativeLink } from "fumadocs-ui/mdx";
14
+ import { gitConfig } from "@/lib/shared";
15
+
16
+ export default async function Page(props: PageProps<"/[[...slug]]">) {
17
+ const params = await props.params;
18
+ const page = source.getPage(params.slug);
19
+ if (!page) notFound();
20
+
21
+ const MDX = page.data.body;
22
+ const markdownUrl = getPageMarkdownUrl(page).url;
23
+
24
+ return (
25
+ <DocsPage toc={page.data.toc} full={page.data.full}>
26
+ <DocsTitle>{page.data.title}</DocsTitle>
27
+ <DocsDescription className="mb-0">
28
+ {page.data.description}
29
+ </DocsDescription>
30
+ <div className="flex flex-row items-center gap-2 border-b pb-6">
31
+ <MarkdownCopyButton markdownUrl={markdownUrl} />
32
+ <ViewOptionsPopover
33
+ markdownUrl={markdownUrl}
34
+ githubUrl={`https://github.com/${gitConfig.user}/${gitConfig.repo}/blob/${gitConfig.branch}/content/docs/${page.path}`}
35
+ />
36
+ </div>
37
+ <DocsBody>
38
+ <MDX
39
+ components={getMDXComponents({
40
+ // this allows you to link to other pages with relative file paths
41
+ a: createRelativeLink(source, page),
42
+ })}
43
+ />
44
+ </DocsBody>
45
+ </DocsPage>
46
+ );
47
+ }
48
+
49
+ export async function generateStaticParams() {
50
+ return source.generateParams();
51
+ }
52
+
53
+ export async function generateMetadata(
54
+ props: PageProps<"/[[...slug]]">,
55
+ ): Promise<Metadata> {
56
+ const params = await props.params;
57
+ const page = source.getPage(params.slug);
58
+ if (!page) notFound();
59
+
60
+ return {
61
+ title: page.data.title,
62
+ description: page.data.description,
63
+ openGraph: {
64
+ images: getPageImage(page).url,
65
+ },
66
+ };
67
+ }
apps/docs/app/api/search/route.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { source } from "@/lib/source";
2
+ import { createFromSource } from "fumadocs-core/search/server";
3
+
4
+ export const { GET } = createFromSource(source, {
5
+ // https://docs.orama.com/docs/orama-js/supported-languages
6
+ language: "english",
7
+ });
apps/docs/app/global.css ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+ @import "fumadocs-ui/css/neutral.css";
3
+ @import "fumadocs-ui/css/preset.css";
4
+
5
+ html {
6
+ scrollbar-gutter: stable;
7
+ }
8
+
9
+ html > body[data-scroll-locked] {
10
+ margin-right: 0px !important;
11
+ --removed-body-scroll-bar-size: 0px !important;
12
+ }
apps/docs/app/layout.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { RootProvider } from "fumadocs-ui/provider/next";
3
+ import { DocsLayout } from "fumadocs-ui/layouts/docs";
4
+ import "./global.css";
5
+ import { Inter } from "next/font/google";
6
+ import { source } from "@/lib/source";
7
+ import { baseOptions } from "@/lib/layout.shared";
8
+
9
+ const inter = Inter({
10
+ subsets: ["latin"],
11
+ });
12
+
13
+ export const metadata: Metadata = {
14
+ metadataBase: new URL("https://docs.ttsarena.org"),
15
+ title: {
16
+ default: "TTS Arena Docs",
17
+ template: "%s · TTS Arena Docs",
18
+ },
19
+ description:
20
+ "Documentation for TTS Arena — the crowdsourced text-to-speech benchmark.",
21
+ };
22
+
23
+ export default function Layout({ children }: LayoutProps<"/">) {
24
+ return (
25
+ <html lang="en" className={inter.className} suppressHydrationWarning>
26
+ <body className="flex min-h-screen flex-col">
27
+ <RootProvider>
28
+ {/* Docs are the whole site — the DocsLayout (sidebar + nav) wraps
29
+ everything at the root; there is no separate homepage. */}
30
+ <DocsLayout tree={source.getPageTree()} {...baseOptions()}>
31
+ {children}
32
+ </DocsLayout>
33
+ </RootProvider>
34
+ </body>
35
+ </html>
36
+ );
37
+ }
apps/docs/app/llms-full.txt/route.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getLLMText, source } from "@/lib/source";
2
+
3
+ export const revalidate = false;
4
+
5
+ export async function GET() {
6
+ const scan = source.getPages().map(getLLMText);
7
+ const scanned = await Promise.all(scan);
8
+
9
+ return new Response(scanned.join("\n\n"));
10
+ }
apps/docs/app/llms.mdx/docs/[[...slug]]/route.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getLLMText, getPageMarkdownUrl, source } from "@/lib/source";
2
+ import { notFound } from "next/navigation";
3
+
4
+ export const revalidate = false;
5
+
6
+ export async function GET(
7
+ _req: Request,
8
+ { params }: RouteContext<"/llms.mdx/docs/[[...slug]]">,
9
+ ) {
10
+ const { slug } = await params;
11
+ const page = source.getPage(slug?.slice(0, -1));
12
+ if (!page) notFound();
13
+
14
+ return new Response(await getLLMText(page), {
15
+ headers: {
16
+ "Content-Type": "text/markdown",
17
+ },
18
+ });
19
+ }
20
+
21
+ export function generateStaticParams() {
22
+ return source.getPages().map((page) => ({
23
+ lang: page.locale,
24
+ slug: getPageMarkdownUrl(page).segments,
25
+ }));
26
+ }
apps/docs/app/llms.txt/route.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { source } from "@/lib/source";
2
+ import { llms } from "fumadocs-core/source";
3
+
4
+ export const revalidate = false;
5
+
6
+ export function GET() {
7
+ return new Response(llms(source).index());
8
+ }
apps/docs/app/og/docs/[...slug]/route.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getPageImage, source } from "@/lib/source";
2
+ import { notFound } from "next/navigation";
3
+ import { ImageResponse } from "next/og";
4
+ import { generate as DefaultImage } from "fumadocs-ui/og";
5
+ import { appName } from "@/lib/shared";
6
+
7
+ export const revalidate = false;
8
+
9
+ export async function GET(
10
+ _req: Request,
11
+ { params }: RouteContext<"/og/docs/[...slug]">,
12
+ ) {
13
+ const { slug } = await params;
14
+ const page = source.getPage(slug.slice(0, -1));
15
+ if (!page) notFound();
16
+
17
+ return new ImageResponse(
18
+ <DefaultImage
19
+ title={page.data.title}
20
+ description={page.data.description}
21
+ site={appName}
22
+ />,
23
+ {
24
+ width: 1200,
25
+ height: 630,
26
+ },
27
+ );
28
+ }
29
+
30
+ export function generateStaticParams() {
31
+ return source.getPages().map((page) => ({
32
+ lang: page.locale,
33
+ slug: getPageImage(page).segments,
34
+ }));
35
+ }
apps/docs/components/mdx.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import defaultMdxComponents from "fumadocs-ui/mdx";
2
+ import type { MDXComponents } from "mdx/types";
3
+
4
+ export function getMDXComponents(components?: MDXComponents) {
5
+ return {
6
+ ...defaultMdxComponents,
7
+ ...components,
8
+ } satisfies MDXComponents;
9
+ }
10
+
11
+ export const useMDXComponents = getMDXComponents;
12
+
13
+ declare global {
14
+ type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
15
+ }
apps/docs/content/docs/api.mdx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Provider API
3
+ description: The contract your TTS endpoint needs to join the arena.
4
+ ---
5
+
6
+ The arena talks to your model through a small HTTP contract. Our router calls
7
+ your endpoint server-side, downloads the audio, and proxies it to the client -
8
+ so the response never reaches the browser directly and your keys stay private.
9
+
10
+ ## The request
11
+
12
+ We send a line of text and (optionally) a voice id. Your endpoint synthesizes it
13
+ and returns audio. A typical shape:
14
+
15
+ ```http
16
+ POST https://your-api.example.com/tts
17
+ Authorization: Bearer <key>
18
+ Content-Type: application/json
19
+
20
+ {
21
+ "text": "The quick brown fox jumps over the lazy dog.",
22
+ "voice_id": "one-of-your-voice-ids"
23
+ }
24
+ ```
25
+
26
+ The exact field names are flexible - we adapt a small provider adapter per
27
+ model. What matters is that, given text, you return speech.
28
+
29
+ ## The response
30
+
31
+ Either is fine:
32
+
33
+ - **Raw audio bytes** (`audio/mpeg`, `audio/wav`, …) returned directly, or
34
+ - **JSON** containing base64 audio or a public URL we can download.
35
+
36
+ Common formats (mp3, wav, ogg, flac, opus) all work; we normalize on our side.
37
+
38
+ ## Voices
39
+
40
+ The arena cycles a **fixed pool of voices** rather than cloning. Provide a list
41
+ of voice ids and we rotate through them across battles, so a model is judged
42
+ across its range rather than a single voice.
43
+
44
+ ## Reliability
45
+
46
+ - Aim to respond within ~15-30s for a sentence-length prompt.
47
+ - Return a non-2xx (or an error payload) on failure - we record it, retry with
48
+ another model, and surface failing models in our admin tooling.
49
+
50
+ <Callout type="info">
51
+ Latency and success rate are tracked per model. A model that fails frequently
52
+ can be temporarily timed out so it doesn't disrupt battles.
53
+ </Callout>
apps/docs/content/docs/development.mdx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Development
3
+ description: Run TTS Arena locally and find your way around the codebase.
4
+ ---
5
+
6
+ TTS Arena is a [Bun](https://bun.sh) monorepo. The web app is Next.js; the
7
+ router is a separate Hono service that talks to providers.
8
+
9
+ ## Prerequisites
10
+
11
+ - [Bun](https://bun.sh) 1.1.42
12
+ - A Postgres database
13
+ - A Hugging Face OAuth app for sign-in
14
+ ([create one](https://huggingface.co/settings/applications/new))
15
+
16
+ ## Setup
17
+
18
+ ```bash
19
+ git clone https://github.com/TTS-AGI/TTS-Arena
20
+ cd TTS-Arena
21
+ bun install
22
+ ```
23
+
24
+ Point `DATABASE_URL` at your Postgres instance, then create the schema and seed
25
+ a starter set of models:
26
+
27
+ ```bash
28
+ cd apps/web
29
+ bun run db:migrate
30
+ bun run db:seed
31
+ ```
32
+
33
+ ## Configuration
34
+
35
+ Set these in the environment (e.g. an `.env` the web app can read):
36
+
37
+ | Variable | Required | Notes |
38
+ | ------------------------ | -------- | ------------------------------------------------------------------------- |
39
+ | `DATABASE_URL` | yes | Postgres connection string. Add `?sslmode=require` for a TLS-only server. |
40
+ | `HF_OAUTH_CLIENT_ID` | yes | From your Hugging Face OAuth app. |
41
+ | `HF_OAUTH_CLIENT_SECRET` | yes | Same app. |
42
+ | `SESSION_SECRET` | yes | Any long random string. |
43
+ | `ADMIN_USERS` | no | Comma-separated HF usernames with admin access. |
44
+ | `ROUTER_URL` | no | Router base URL. Defaults to `http://localhost:8080`. |
45
+ | `APP_URL` | no | Public app URL. Defaults to `http://localhost:3000`. |
46
+ | `SECURITY_DISABLED` | no | Set to `1` to turn off the anti-fraud gate in local dev. |
47
+
48
+ Provider API keys are read by the router from the environment too - add them as
49
+ you wire up providers.
50
+
51
+ ## Running
52
+
53
+ ```bash
54
+ bun run dev # web app on :3000
55
+ bun run dev:router # router on :8080
56
+ ```
57
+
58
+ ## Useful scripts
59
+
60
+ Run from the repo root:
61
+
62
+ ```bash
63
+ bun run build # build every workspace
64
+ bun run lint # lint every workspace
65
+ bun run typecheck # type-check every workspace
66
+ bun test # run tests
67
+ bun run format # prettier --write
68
+ ```
69
+
70
+ And from `apps/web` for database work:
71
+
72
+ ```bash
73
+ bun run db:generate # generate a migration from schema changes
74
+ bun run db:migrate # apply migrations
75
+ bun run db:seed # seed models from the provider packages
76
+ bun run db:recompute # replay clean votes and rebuild ratings
77
+ ```
78
+
79
+ ## Layout
80
+
81
+ ```
82
+ apps/web Next.js app - arena, leaderboard, admin
83
+ apps/router Hono service that calls TTS providers
84
+ apps/docs this documentation site (Fumadocs)
85
+ packages/shared shared types + the rating math
86
+ packages/provider-sdk provider interface + registry
87
+ packages/providers/* individual public providers
88
+ ```
89
+
90
+ The rating logic lives in `packages/shared` - `bradley-terry.ts` and
91
+ `glicko.ts` - with tests alongside. See [Ranking](/ranking) for how it's used.
92
+
93
+ ## Contributing
94
+
95
+ Issues and pull requests are welcome on
96
+ [GitHub](https://github.com/TTS-AGI/TTS-Arena). The project is licensed under
97
+ [Apache 2.0](https://github.com/TTS-AGI/TTS-Arena/blob/main/LICENSE).
apps/docs/content/docs/index.mdx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Introduction
3
+ description: A crowdsourced, blind benchmark for text-to-speech.
4
+ ---
5
+
6
+ **TTS Arena** ranks text-to-speech models by ear. You type a line, two anonymous
7
+ models read it back, and you pick the one that sounds more human. Each vote
8
+ feeds the leaderboard.
9
+
10
+ The models stay hidden until you've voted, so the choice is about the audio, not
11
+ the name attached to it.
12
+
13
+ [**Open the arena and vote →**](https://huggingface.co/spaces/TTS-AGI/TTS-Arena-V2)
14
+
15
+ ## Why
16
+
17
+ There hasn't been a good way to measure how natural a synthetic voice sounds.
18
+ Word error rate tells you whether speech is intelligible, not whether it sounds
19
+ alive. Mean opinion scores rely on a small panel in a lab. TTS Arena uses
20
+ large-scale human preference instead - anyone can listen, compare, and vote, and
21
+ the resulting leaderboard is open.
22
+
23
+ ## Start here
24
+
25
+ <Cards>
26
+ <Card title="Voting" href="/voting">
27
+ How to vote and the rules that keep the board fair.
28
+ </Card>
29
+ <Card title="Ranking" href="/ranking">
30
+ How votes become a leaderboard.
31
+ </Card>
32
+ <Card title="Submit a model" href="/submit-a-model">
33
+ Add your model, publicly or under a codename.
34
+ </Card>
35
+ <Card title="Provider API" href="/api">
36
+ The HTTP contract your TTS endpoint needs to meet.
37
+ </Card>
38
+ </Cards>
39
+
40
+ ## Quick facts
41
+
42
+ - Sign in with Hugging Face to vote; accounts must be at least 30 days old.
43
+ - Prompts are English-only for now, capped at 1,000 characters.
44
+ - Models are revealed only after you vote.
45
+ - TTS Arena is open source under Apache 2.0 -
46
+ [source on GitHub](https://github.com/TTS-AGI/TTS-Arena).
apps/docs/content/docs/meta.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "Docs",
3
+ "pages": [
4
+ "index",
5
+ "voting",
6
+ "ranking",
7
+ "submit-a-model",
8
+ "api",
9
+ "development"
10
+ ]
11
+ }
apps/docs/content/docs/ranking.mdx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Ranking
3
+ description: How votes become a leaderboard.
4
+ ---
5
+
6
+ Each vote is one head-to-head result: model A beat model B on this prompt. The
7
+ leaderboard is what falls out when you fit all of those results together.
8
+
9
+ ## The rating
10
+
11
+ We rank models with a [Bradley–Terry](https://en.wikipedia.org/wiki/Bradley%E2%80%93Terry_model)
12
+ model - a standard way to turn pairwise wins and losses into a single strength
13
+ score per competitor. It's fit over the entire vote history at once, so a
14
+ model's rating reflects every matchup it has been in, not just its recent ones.
15
+ The result doesn't depend on the order votes arrived in.
16
+
17
+ Ratings are centered around 1500, so the numbers read like familiar Elo scores.
18
+ The fit is cached and refreshed as votes come in.
19
+
20
+ ## Rank by the lower bound
21
+
22
+ A model's rating comes with a confidence interval - wide when there's little
23
+ data, narrow once it has played a lot. We show the rating but **sort by the
24
+ bottom of that interval**.
25
+
26
+ The effect: a model can't shoot to the top off a handful of lucky wins. It has
27
+ to be both good and well-tested to rank highly. Each row shows a `±` next to its
28
+ rating so you can see how settled it is.
29
+
30
+ ## New models
31
+
32
+ - A model needs **100 votes** to appear on the board. Below that the rating
33
+ swings too much to mean anything.
34
+ - Under **300 votes** it's marked **Preliminary** - ranked normally, but still
35
+ moving.
36
+ - Brand-new models with only a few votes are hidden by default. Tick **Show new
37
+ models with few votes** on the leaderboard to see them, with wide error bars.
38
+
39
+ To keep tiny samples from producing absurd numbers (a model that has only ever
40
+ won would otherwise rate infinitely high), the fit is lightly regularized toward
41
+ the average. The nudge fades as real votes accumulate and is gone within a few
42
+ hundred.
43
+
44
+ ## Why blind and pairwise
45
+
46
+ Knowing a model's name changes how people hear it, so identities stay hidden
47
+ until after the vote. And "which of these two is better?" is a far easier and
48
+ more reliable judgment than scoring a single clip out of context - it's how
49
+ listening tests have always been run.
50
+
51
+ <Callout type="info">
52
+ Only clean votes count. Votes flagged by the anti-fraud system, and votes from
53
+ quarantined accounts, are left out of the fit. See [Voting](/voting).
54
+ </Callout>
apps/docs/content/docs/submit-a-model.mdx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Submit a model
3
+ description: Add your TTS model for evaluation - public or anonymous.
4
+ ---
5
+
6
+ Want your model on the board? Open an issue on [GitHub](https://github.com/TTS-AGI/TTS-Arena), join our [Discord](https://discord.gg/HB8fMR6GTr), or [email us](mailto:me@mrfake.name) and include an
7
+ API endpoint and key (see the [Provider API](/api)).
8
+
9
+ ## What we need
10
+
11
+ - An HTTP endpoint that synthesizes a line of text and returns audio.
12
+ - A pool of voices to rotate through (we don't do zero-shot cloning in the
13
+ arena - we cycle a fixed set).
14
+ - A display name for the leaderboard.
15
+
16
+ ## Anonymous pre-release
17
+
18
+ You can run under a codename before your model is public - a way to get honest,
19
+ blind feedback ahead of launch. Two rules keep this fair:
20
+
21
+ - Anonymity is **time/exposure-bounded, not performance-bounded.** A codenamed
22
+ entry is revealed after a set period or vote count regardless of how it does -
23
+ you can withdraw it before then, but you can't sit on the board indefinitely
24
+ or reveal it _only because it won_.
25
+ - **Permanent anonymity is for genuinely unreleased models.** A model that's
26
+ already publicly available runs under its real name (a codename for a fixed
27
+ window is fine).
28
+
29
+ <Callout type="warn">
30
+ To keep comparisons fair, we don't evaluate multiple versions of the same
31
+ model simultaneously.
32
+ </Callout>
33
+
34
+ ## Stealth models
35
+
36
+ Anonymous entries appear on the leaderboard under their codename with a neutral
37
+ mark. Clicking one explains it's a stealth model in evaluation - its identity
38
+ stays hidden until it's revealed.
apps/docs/content/docs/voting.mdx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Voting
3
+ description: What makes a vote count, and the rules that keep the board fair.
4
+ ---
5
+
6
+ ## Casting a vote
7
+
8
+ 1. Type a line, or hit **Random** for one from the prompt pool.
9
+ 2. Two anonymous models - **A** and **B** - synthesize it.
10
+ 3. Listen to both, then pick the one that sounds more human. One choice, no
11
+ skips.
12
+ 4. Both models' ratings update, and their identities are revealed.
13
+
14
+ You need to listen to enough of each clip before voting unlocks - this keeps
15
+ votes grounded in the audio rather than reflexive clicks.
16
+
17
+ ## Requirements
18
+
19
+ - **Sign in with Hugging Face.** Voting is tied to your account so each vote
20
+ counts once. Accounts must be at least **30 days old**.
21
+ - **English only**, for now - it's the language all models support. Multilingual
22
+ is on the roadmap.
23
+ - Prompts are capped at **1,000 characters**.
24
+
25
+ ## Keeping it fair
26
+
27
+ Votes run through an anti-abuse system so the board reflects real preferences:
28
+
29
+ - **Behavioral signals** score each vote for risk (timing, patterns, device and
30
+ network signals). High-risk votes are recorded but **shadow-excluded** - they
31
+ never move the public ratings.
32
+ - A lightweight **proof-of-work captcha** appears once per session, and again if
33
+ risk rises.
34
+ - A background sweep looks for coordinated rings (many accounts sharing an IP or
35
+ fingerprint piling onto one model) and per-account bias, retroactively
36
+ excluding suspicious votes and recomputing the board from the clean set.
37
+
38
+ <Callout>
39
+ None of this affects honest voting - it's invisible unless your activity looks
40
+ automated or coordinated.
41
+ </Callout>
apps/docs/lib/cn.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ export { twMerge as cn } from "tailwind-merge";
apps/docs/lib/layout.shared.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
2
+ import { AudioLines } from "lucide-react";
3
+ import { appName, arenaUrl, gitConfig } from "./shared";
4
+
5
+ export function baseOptions(): BaseLayoutProps {
6
+ return {
7
+ nav: {
8
+ title: (
9
+ <>
10
+ <AudioLines className="text-fd-primary size-5" />
11
+ <span className="font-semibold">{appName}</span>
12
+ </>
13
+ ),
14
+ },
15
+ links: [
16
+ {
17
+ text: "Open the Arena",
18
+ url: arenaUrl,
19
+ external: true,
20
+ },
21
+ ],
22
+ githubUrl: `https://github.com/${gitConfig.user}/${gitConfig.repo}`,
23
+ };
24
+ }
apps/docs/lib/shared.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const appName = "TTS Arena Docs";
2
+ // Docs are served at the root — there's no separate homepage.
3
+ export const docsRoute = "/";
4
+ export const docsImageRoute = "/og/docs";
5
+ export const docsContentRoute = "/llms.mdx/docs";
6
+
7
+ /** The main arena, linked from the docs nav. */
8
+ export const arenaUrl = "https://huggingface.co/spaces/TTS-AGI/TTS-Arena-V2";
9
+
10
+ export const gitConfig = {
11
+ user: "TTS-AGI",
12
+ repo: "TTS-Arena",
13
+ branch: "main",
14
+ };
apps/docs/lib/source.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { docs } from "collections/server";
2
+ import { loader } from "fumadocs-core/source";
3
+ import { lucideIconsPlugin } from "fumadocs-core/source/lucide-icons";
4
+ import { docsContentRoute, docsImageRoute, docsRoute } from "./shared";
5
+
6
+ // See https://fumadocs.dev/docs/headless/source-api for more info
7
+ export const source = loader({
8
+ baseUrl: docsRoute,
9
+ source: docs.toFumadocsSource(),
10
+ plugins: [lucideIconsPlugin()],
11
+ });
12
+
13
+ export function getPageImage(page: (typeof source)["$inferPage"]) {
14
+ const segments = [...page.slugs, "image.png"];
15
+
16
+ return {
17
+ segments,
18
+ url: `${docsImageRoute}/${segments.join("/")}`,
19
+ };
20
+ }
21
+
22
+ export function getPageMarkdownUrl(page: (typeof source)["$inferPage"]) {
23
+ const segments = [...page.slugs, "content.md"];
24
+
25
+ return {
26
+ segments,
27
+ url: `${docsContentRoute}/${segments.join("/")}`,
28
+ };
29
+ }
30
+
31
+ export async function getLLMText(page: (typeof source)["$inferPage"]) {
32
+ const processed = await page.data.getText("processed");
33
+
34
+ return `# ${page.data.title} (${page.url})
35
+
36
+ ${processed}`;
37
+ }
apps/docs/next.config.mjs ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createMDX } from "fumadocs-mdx/next";
2
+
3
+ const withMDX = createMDX();
4
+
5
+ /** @type {import('next').NextConfig} */
6
+ const config = {
7
+ reactStrictMode: true,
8
+ // Self-contained server bundle for the Docker/HF Space deploy. Built in
9
+ // isolation in Docker (only apps/docs present, no parent workspace), so Next
10
+ // traces from the app dir and server.js lands at the standalone root. No
11
+ // explicit tracing root — that's only needed for monorepo builds, and a wrong
12
+ // value trips Turbopack's workspace-root inference.
13
+ output: "standalone",
14
+ typescript: {
15
+ // The page uses fumadocs' generated MDX page-data fields (body/toc/getText)
16
+ // whose types are injected by the MDX plugin at build time and aren't
17
+ // visible to a plain tsc pass — it's the template's own pattern. The MDX
18
+ // still compiles and renders fine; don't fail the build on this gap. (The
19
+ // docs app is content + template boilerplate, so there's little else to
20
+ // type-check here anyway.)
21
+ ignoreBuildErrors: true,
22
+ },
23
+ };
24
+
25
+ export default withMDX(config);
apps/docs/package.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@ttsa/docs",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "fumadocs-mdx && next build --webpack",
8
+ "dev": "fumadocs-mdx && next dev",
9
+ "start": "next start",
10
+ "typecheck": "echo 'docs: typecheck handled by next build (fumadocs MDX types are build-time)'",
11
+ "lint": "echo 'no lint for docs'"
12
+ },
13
+ "dependencies": {
14
+ "fumadocs-core": "16.9.3",
15
+ "fumadocs-mdx": "15.0.11",
16
+ "fumadocs-ui": "16.9.3",
17
+ "lucide-react": "^1.17.0",
18
+ "next": "16.2.7",
19
+ "react": "^19.2.0",
20
+ "react-dom": "^19.2.0",
21
+ "tailwind-merge": "^3.6.0"
22
+ },
23
+ "devDependencies": {
24
+ "@tailwindcss/postcss": "^4.3.0",
25
+ "@types/mdx": "^2.0.13",
26
+ "@types/node": "^25.9.1",
27
+ "@types/react": "^19.2.0",
28
+ "@types/react-dom": "^19.2.0",
29
+ "postcss": "^8.5.15",
30
+ "tailwindcss": "^4.3.0",
31
+ "typescript": "^6.0.3"
32
+ }
33
+ }
apps/docs/postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
apps/docs/proxy.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { isMarkdownPreferred, rewritePath } from "fumadocs-core/negotiation";
3
+ import { docsContentRoute, docsRoute } from "@/lib/shared";
4
+
5
+ const { rewrite: rewriteDocs } = rewritePath(
6
+ `${docsRoute}{/*path}`,
7
+ `${docsContentRoute}{/*path}/content.md`,
8
+ );
9
+ const { rewrite: rewriteSuffix } = rewritePath(
10
+ `${docsRoute}{/*path}.md`,
11
+ `${docsContentRoute}{/*path}/content.md`,
12
+ );
13
+
14
+ export default function proxy(request: NextRequest) {
15
+ const result = rewriteSuffix(request.nextUrl.pathname);
16
+ if (result) {
17
+ return NextResponse.rewrite(new URL(result, request.nextUrl));
18
+ }
19
+
20
+ if (isMarkdownPreferred(request)) {
21
+ const result = rewriteDocs(request.nextUrl.pathname);
22
+
23
+ if (result) {
24
+ return NextResponse.rewrite(new URL(result, request.nextUrl));
25
+ }
26
+ }
27
+
28
+ return NextResponse.next();
29
+ }
apps/docs/public/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+ # Static assets for the docs site.
apps/docs/source.config.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, defineDocs } from "fumadocs-mdx/config";
2
+ import { metaSchema, pageSchema } from "fumadocs-core/source/schema";
3
+
4
+ // You can customize Zod schemas for frontmatter and `meta.json` here
5
+ // see https://fumadocs.dev/docs/mdx/collections
6
+ export const docs = defineDocs({
7
+ dir: "content/docs",
8
+ docs: {
9
+ schema: pageSchema,
10
+ postprocess: {
11
+ includeProcessedMarkdown: true,
12
+ },
13
+ },
14
+ meta: {
15
+ schema: metaSchema,
16
+ },
17
+ });
18
+
19
+ export default defineConfig({
20
+ mdxOptions: {
21
+ // MDX options
22
+ },
23
+ });
apps/docs/tsconfig.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "module": "esnext",
12
+ "moduleResolution": "bundler",
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "jsx": "react-jsx",
16
+ "incremental": true,
17
+ "paths": {
18
+ "@/*": ["./*"],
19
+ "collections/*": ["./.source/*"]
20
+ },
21
+ "plugins": [
22
+ {
23
+ "name": "next"
24
+ }
25
+ ]
26
+ },
27
+ "include": [
28
+ "next-env.d.ts",
29
+ "**/*.ts",
30
+ "**/*.tsx",
31
+ ".next/types/**/*.ts",
32
+ ".next/dev/types/**/*.ts"
33
+ ],
34
+ "exclude": ["node_modules"]
35
+ }