GitHub Actions commited on
Commit ·
00954e2
0
Parent(s):
Deploy docs from github.com/TTS-AGI/TTS-Arena@d7c27b1
Browse files- Dockerfile +46 -0
- README.md +19 -0
- apps/docs/.gitignore +26 -0
- apps/docs/app/[[...slug]]/page.tsx +67 -0
- apps/docs/app/api/search/route.ts +7 -0
- apps/docs/app/global.css +12 -0
- apps/docs/app/layout.tsx +37 -0
- apps/docs/app/llms-full.txt/route.ts +10 -0
- apps/docs/app/llms.mdx/docs/[[...slug]]/route.ts +26 -0
- apps/docs/app/llms.txt/route.ts +8 -0
- apps/docs/app/og/docs/[...slug]/route.tsx +35 -0
- apps/docs/components/mdx.tsx +15 -0
- apps/docs/content/docs/api.mdx +53 -0
- apps/docs/content/docs/development.mdx +97 -0
- apps/docs/content/docs/index.mdx +46 -0
- apps/docs/content/docs/meta.json +11 -0
- apps/docs/content/docs/ranking.mdx +54 -0
- apps/docs/content/docs/submit-a-model.mdx +38 -0
- apps/docs/content/docs/voting.mdx +41 -0
- apps/docs/lib/cn.ts +1 -0
- apps/docs/lib/layout.shared.tsx +24 -0
- apps/docs/lib/shared.ts +14 -0
- apps/docs/lib/source.ts +37 -0
- apps/docs/next.config.mjs +25 -0
- apps/docs/package.json +33 -0
- apps/docs/postcss.config.mjs +7 -0
- apps/docs/proxy.ts +29 -0
- apps/docs/public/.gitkeep +1 -0
- apps/docs/source.config.ts +23 -0
- apps/docs/tsconfig.json +35 -0
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 |
+
}
|