GitHub Action
commited on
Commit
Β·
33b2a07
1
Parent(s):
1d29ab1
Sync from GitHub: b110eba71afe95ef3e43dd1d91c79c3e172e36f7
Browse files- .gitattributes +6 -0
- format.sh +15 -0
- frontend/.prettierrc +15 -0
- frontend/package-lock.json +29 -0
- frontend/package.json +2 -0
- frontend/src/app.css +9 -9
- frontend/src/app.html +11 -11
- frontend/src/routes/+layout.svelte +96 -72
- frontend/src/routes/+page.svelte +694 -591
- hfstudio/__init__.py +6 -4
- hfstudio/__main__.py +1 -1
- hfstudio/cli.py +74 -53
- hfstudio/server.py +138 -96
- hfstudio/static/_app/immutable/assets/2.D7LovqyU.css +1 -0
- hfstudio/static/_app/immutable/chunks/DVK2ASb7.js +3 -0
- hfstudio/static/_app/immutable/entry/app.CIwzfQ6B.js +2 -0
- hfstudio/static/_app/immutable/entry/start.jV5w9ZfR.js +1 -0
- hfstudio/static/_app/immutable/nodes/0.UMzK7bBX.js +5 -0
- hfstudio/static/_app/immutable/nodes/1.i8TJb2hr.js +1 -0
- hfstudio/static/_app/immutable/nodes/2.1jor8E63.js +0 -0
- hfstudio/static/_app/version.json +1 -1
- hfstudio/static/index.html +17 -17
- models/chatterbox/local.py +31 -32
- test_chatterbox.py +2 -2
.gitattributes
CHANGED
|
@@ -7,3 +7,9 @@
|
|
| 7 |
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 8 |
*.webm filter=lfs diff=lfs merge=lfs -text
|
| 9 |
*.pdf filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 8 |
*.webm filter=lfs diff=lfs merge=lfs -text
|
| 9 |
*.pdf filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
hfstudio/static/assets/hf-logo.png filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
hfstudio/static/assets/hf-studio-logo.png filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
frontend/static/assets/hf-logo.png filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
frontend/static/assets/hf-studio-logo.png filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
hfstudio/static/samples/harvard.wav filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
frontend/static/samples/harvard.wav filter=lfs diff=lfs merge=lfs -text
|
format.sh
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# format.sh - Format both backend and frontend code
|
| 4 |
+
|
| 5 |
+
set -e
|
| 6 |
+
|
| 7 |
+
echo "π§ Formatting backend with ruff..."
|
| 8 |
+
ruff format .
|
| 9 |
+
|
| 10 |
+
echo "β¨ Formatting frontend with prettier..."
|
| 11 |
+
cd frontend
|
| 12 |
+
npx prettier --write "src/**/*.{js,ts,svelte,html,css,json}" --config .prettierrc
|
| 13 |
+
cd ..
|
| 14 |
+
|
| 15 |
+
echo "β
All formatting complete!"
|
frontend/.prettierrc
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"useTabs": false,
|
| 3 |
+
"singleQuote": true,
|
| 4 |
+
"trailingComma": "es5",
|
| 5 |
+
"printWidth": 100,
|
| 6 |
+
"plugins": ["prettier-plugin-svelte"],
|
| 7 |
+
"overrides": [
|
| 8 |
+
{
|
| 9 |
+
"files": "*.svelte",
|
| 10 |
+
"options": {
|
| 11 |
+
"parser": "svelte"
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
]
|
| 15 |
+
}
|
frontend/package-lock.json
CHANGED
|
@@ -18,6 +18,8 @@
|
|
| 18 |
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
| 19 |
"autoprefixer": "^10.4.16",
|
| 20 |
"postcss": "^8.4.32",
|
|
|
|
|
|
|
| 21 |
"svelte": "^4.2.7",
|
| 22 |
"tailwindcss": "^3.3.0",
|
| 23 |
"vite": "^5.0.3"
|
|
@@ -2112,6 +2114,33 @@
|
|
| 2112 |
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 2113 |
}
|
| 2114 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2115 |
"node_modules/prismjs": {
|
| 2116 |
"version": "1.30.0",
|
| 2117 |
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
|
|
|
| 18 |
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
| 19 |
"autoprefixer": "^10.4.16",
|
| 20 |
"postcss": "^8.4.32",
|
| 21 |
+
"prettier": "^3.6.2",
|
| 22 |
+
"prettier-plugin-svelte": "^3.4.0",
|
| 23 |
"svelte": "^4.2.7",
|
| 24 |
"tailwindcss": "^3.3.0",
|
| 25 |
"vite": "^5.0.3"
|
|
|
|
| 2114 |
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 2115 |
}
|
| 2116 |
},
|
| 2117 |
+
"node_modules/prettier": {
|
| 2118 |
+
"version": "3.6.2",
|
| 2119 |
+
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
| 2120 |
+
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
| 2121 |
+
"dev": true,
|
| 2122 |
+
"license": "MIT",
|
| 2123 |
+
"bin": {
|
| 2124 |
+
"prettier": "bin/prettier.cjs"
|
| 2125 |
+
},
|
| 2126 |
+
"engines": {
|
| 2127 |
+
"node": ">=14"
|
| 2128 |
+
},
|
| 2129 |
+
"funding": {
|
| 2130 |
+
"url": "https://github.com/prettier/prettier?sponsor=1"
|
| 2131 |
+
}
|
| 2132 |
+
},
|
| 2133 |
+
"node_modules/prettier-plugin-svelte": {
|
| 2134 |
+
"version": "3.4.0",
|
| 2135 |
+
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.0.tgz",
|
| 2136 |
+
"integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==",
|
| 2137 |
+
"dev": true,
|
| 2138 |
+
"license": "MIT",
|
| 2139 |
+
"peerDependencies": {
|
| 2140 |
+
"prettier": "^3.0.0",
|
| 2141 |
+
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
|
| 2142 |
+
}
|
| 2143 |
+
},
|
| 2144 |
"node_modules/prismjs": {
|
| 2145 |
"version": "1.30.0",
|
| 2146 |
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
frontend/package.json
CHANGED
|
@@ -13,6 +13,8 @@
|
|
| 13 |
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
| 14 |
"autoprefixer": "^10.4.16",
|
| 15 |
"postcss": "^8.4.32",
|
|
|
|
|
|
|
| 16 |
"svelte": "^4.2.7",
|
| 17 |
"tailwindcss": "^3.3.0",
|
| 18 |
"vite": "^5.0.3"
|
|
|
|
| 13 |
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
| 14 |
"autoprefixer": "^10.4.16",
|
| 15 |
"postcss": "^8.4.32",
|
| 16 |
+
"prettier": "^3.6.2",
|
| 17 |
+
"prettier-plugin-svelte": "^3.4.0",
|
| 18 |
"svelte": "^4.2.7",
|
| 19 |
"tailwindcss": "^3.3.0",
|
| 20 |
"vite": "^5.0.3"
|
frontend/src/app.css
CHANGED
|
@@ -3,10 +3,10 @@
|
|
| 3 |
@tailwind utilities;
|
| 4 |
|
| 5 |
/* Prism.js Theme - Light with good contrast */
|
| 6 |
-
code[class*=
|
| 7 |
-
pre[class*=
|
| 8 |
-
color: #
|
| 9 |
-
font-family:
|
| 10 |
direction: ltr;
|
| 11 |
text-align: left;
|
| 12 |
white-space: pre;
|
|
@@ -18,7 +18,7 @@ pre[class*="language-"] {
|
|
| 18 |
hyphens: none;
|
| 19 |
}
|
| 20 |
|
| 21 |
-
pre[class*=
|
| 22 |
padding: 1rem;
|
| 23 |
margin: 0;
|
| 24 |
overflow: auto;
|
|
@@ -35,7 +35,7 @@ pre[class*="language-"] {
|
|
| 35 |
}
|
| 36 |
|
| 37 |
.token.punctuation {
|
| 38 |
-
color: #
|
| 39 |
}
|
| 40 |
|
| 41 |
.token.property,
|
|
@@ -88,13 +88,13 @@ pre[class*="language-"] {
|
|
| 88 |
}
|
| 89 |
.slider-hf::-webkit-slider-thumb {
|
| 90 |
@apply appearance-none w-4 h-4 rounded-full cursor-pointer;
|
| 91 |
-
background: linear-gradient(45deg, #
|
| 92 |
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
| 93 |
}
|
| 94 |
|
| 95 |
.slider-hf::-moz-range-thumb {
|
| 96 |
@apply w-4 h-4 rounded-full cursor-pointer border-0;
|
| 97 |
-
background: linear-gradient(45deg, #
|
| 98 |
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
| 99 |
}
|
| 100 |
|
|
@@ -122,4 +122,4 @@ pre[class*="language-"] {
|
|
| 122 |
|
| 123 |
.pause-filled::after {
|
| 124 |
margin-left: 2px;
|
| 125 |
-
}
|
|
|
|
| 3 |
@tailwind utilities;
|
| 4 |
|
| 5 |
/* Prism.js Theme - Light with good contrast */
|
| 6 |
+
code[class*='language-'],
|
| 7 |
+
pre[class*='language-'] {
|
| 8 |
+
color: #393a34;
|
| 9 |
+
font-family: 'Consolas', 'Bitstream Vera Sans Mono', 'Courier New', Courier, monospace;
|
| 10 |
direction: ltr;
|
| 11 |
text-align: left;
|
| 12 |
white-space: pre;
|
|
|
|
| 18 |
hyphens: none;
|
| 19 |
}
|
| 20 |
|
| 21 |
+
pre[class*='language-'] {
|
| 22 |
padding: 1rem;
|
| 23 |
margin: 0;
|
| 24 |
overflow: auto;
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
.token.punctuation {
|
| 38 |
+
color: #393a34;
|
| 39 |
}
|
| 40 |
|
| 41 |
.token.property,
|
|
|
|
| 88 |
}
|
| 89 |
.slider-hf::-webkit-slider-thumb {
|
| 90 |
@apply appearance-none w-4 h-4 rounded-full cursor-pointer;
|
| 91 |
+
background: linear-gradient(45deg, #ffd21e, #ff9d00);
|
| 92 |
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
| 93 |
}
|
| 94 |
|
| 95 |
.slider-hf::-moz-range-thumb {
|
| 96 |
@apply w-4 h-4 rounded-full cursor-pointer border-0;
|
| 97 |
+
background: linear-gradient(45deg, #ffd21e, #ff9d00);
|
| 98 |
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
| 99 |
}
|
| 100 |
|
|
|
|
| 122 |
|
| 123 |
.pause-filled::after {
|
| 124 |
margin-left: 2px;
|
| 125 |
+
}
|
frontend/src/app.html
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
<html lang="en">
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<link rel="icon" href="/assets/hf-studio-logo.png" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 7 |
+
<title>HFStudio - Text to Speech</title>
|
| 8 |
+
%sveltekit.head%
|
| 9 |
+
</head>
|
| 10 |
+
<body data-sveltekit-preload-data="hover">
|
| 11 |
+
<div style="display: contents">%sveltekit.body%</div>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/src/routes/+layout.svelte
CHANGED
|
@@ -2,19 +2,21 @@
|
|
| 2 |
import '../app.css';
|
| 3 |
import { Home, Settings, History, Github, Menu } from 'lucide-svelte';
|
| 4 |
import { onMount } from 'svelte';
|
| 5 |
-
|
| 6 |
let currentPage = 'tts';
|
| 7 |
let sidebarOpen = true;
|
| 8 |
-
|
| 9 |
// Initialize from cache immediately to avoid "Checking..." flash
|
| 10 |
const initToken = typeof window !== 'undefined' ? localStorage.getItem('hf_access_token') : null;
|
| 11 |
-
const initCachedToken =
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
| 14 |
let isLoggedIn = false;
|
| 15 |
let username = '';
|
| 16 |
let isCheckingAuth = false; // Start as false by default
|
| 17 |
-
|
| 18 |
// Check if we have valid cached data
|
| 19 |
let hasCachedData = false;
|
| 20 |
if (initToken && initToken === initCachedToken && initCachedUserInfo) {
|
|
@@ -31,13 +33,13 @@
|
|
| 31 |
// We have a token but no valid cache, need to check
|
| 32 |
isCheckingAuth = true;
|
| 33 |
}
|
| 34 |
-
|
| 35 |
let showTokenInput = false;
|
| 36 |
let tokenInput = '';
|
| 37 |
let tokenError = '';
|
| 38 |
let isLocalEnvironment = false;
|
| 39 |
let showSignInPopover = false;
|
| 40 |
-
|
| 41 |
onMount(() => {
|
| 42 |
window.addEventListener('show-signin-popover', () => {
|
| 43 |
showSignInPopover = true;
|
|
@@ -45,7 +47,7 @@
|
|
| 45 |
showSignInPopover = false;
|
| 46 |
}, 4000);
|
| 47 |
});
|
| 48 |
-
|
| 49 |
// Only check if we don't already have valid cached info or need to verify
|
| 50 |
if (!hasCachedData && initToken) {
|
| 51 |
checkLocalTokenAvailability();
|
|
@@ -54,45 +56,45 @@
|
|
| 54 |
// No token at all, check for local availability
|
| 55 |
checkLocalTokenAvailability();
|
| 56 |
}
|
| 57 |
-
|
| 58 |
document.addEventListener('visibilitychange', () => {
|
| 59 |
if (!document.hidden) {
|
| 60 |
checkLoginStatus();
|
| 61 |
}
|
| 62 |
});
|
| 63 |
-
|
| 64 |
window.addEventListener('storage', checkLoginStatus);
|
| 65 |
-
|
| 66 |
const interval = setInterval(checkLoginStatus, 1000);
|
| 67 |
-
|
| 68 |
return () => {
|
| 69 |
window.removeEventListener('storage', checkLoginStatus);
|
| 70 |
clearInterval(interval);
|
| 71 |
};
|
| 72 |
});
|
| 73 |
-
|
| 74 |
async function checkLocalTokenAvailability() {
|
| 75 |
// Skip if we already have valid cached info
|
| 76 |
if (isLoggedIn && hasCachedData) {
|
| 77 |
return;
|
| 78 |
}
|
| 79 |
-
|
| 80 |
isCheckingAuth = true;
|
| 81 |
-
|
| 82 |
try {
|
| 83 |
const response = await fetch('/api/auth/local-token');
|
| 84 |
const data = await response.json();
|
| 85 |
-
|
| 86 |
if (data.available) {
|
| 87 |
isLocalEnvironment = true;
|
| 88 |
localStorage.setItem('hf_access_token', data.token);
|
| 89 |
-
|
| 90 |
-
if (data.user_info && data.user_info.name !==
|
| 91 |
isLoggedIn = true;
|
| 92 |
username = data.user_info.name.split(' ')[0];
|
| 93 |
} else {
|
| 94 |
isLoggedIn = true;
|
| 95 |
-
username =
|
| 96 |
}
|
| 97 |
} else {
|
| 98 |
isLocalEnvironment = false;
|
|
@@ -108,7 +110,7 @@
|
|
| 108 |
const token = localStorage.getItem('hf_access_token');
|
| 109 |
const cachedUserInfo = localStorage.getItem('hf_user_info');
|
| 110 |
const cachedToken = localStorage.getItem('hf_cached_token');
|
| 111 |
-
|
| 112 |
if (token) {
|
| 113 |
// If token hasn't changed and we have cached info, use it
|
| 114 |
if (token === cachedToken && cachedUserInfo) {
|
|
@@ -133,23 +135,24 @@
|
|
| 133 |
localStorage.removeItem('hf_cached_token');
|
| 134 |
}
|
| 135 |
}
|
| 136 |
-
|
| 137 |
async function fetchUserInfo(token) {
|
| 138 |
isCheckingAuth = true;
|
| 139 |
-
|
| 140 |
try {
|
| 141 |
const response = await fetch('https://huggingface.co/api/whoami-v2', {
|
| 142 |
headers: {
|
| 143 |
-
|
| 144 |
-
}
|
| 145 |
});
|
| 146 |
-
|
| 147 |
if (response.ok) {
|
| 148 |
const userData = await response.json();
|
| 149 |
isLoggedIn = true;
|
| 150 |
-
const fullName =
|
|
|
|
| 151 |
username = fullName.split(' ')[0];
|
| 152 |
-
|
| 153 |
// Cache the user info and token
|
| 154 |
const userInfo = { username, fullName };
|
| 155 |
localStorage.setItem('hf_user_info', JSON.stringify(userInfo));
|
|
@@ -185,7 +188,7 @@
|
|
| 185 |
isCheckingAuth = false;
|
| 186 |
}
|
| 187 |
}
|
| 188 |
-
|
| 189 |
async function handleAuthAction() {
|
| 190 |
if (isLoggedIn) {
|
| 191 |
localStorage.removeItem('hf_access_token');
|
|
@@ -195,7 +198,10 @@
|
|
| 195 |
isLoggedIn = false;
|
| 196 |
username = '';
|
| 197 |
} else {
|
| 198 |
-
if (
|
|
|
|
|
|
|
|
|
|
| 199 |
try {
|
| 200 |
const response = await fetch('/api/auth/oauth-config');
|
| 201 |
const config = await response.json();
|
|
@@ -235,8 +241,8 @@
|
|
| 235 |
try {
|
| 236 |
const response = await fetch('https://huggingface.co/api/whoami-v2', {
|
| 237 |
headers: {
|
| 238 |
-
|
| 239 |
-
}
|
| 240 |
});
|
| 241 |
|
| 242 |
if (response.ok) {
|
|
@@ -244,14 +250,15 @@
|
|
| 244 |
const token = tokenInput.trim();
|
| 245 |
localStorage.setItem('hf_access_token', token);
|
| 246 |
isLoggedIn = true;
|
| 247 |
-
const fullName =
|
|
|
|
| 248 |
username = fullName.split(' ')[0];
|
| 249 |
-
|
| 250 |
// Cache the user info and token
|
| 251 |
const userInfo = { username, fullName };
|
| 252 |
localStorage.setItem('hf_user_info', JSON.stringify(userInfo));
|
| 253 |
localStorage.setItem('hf_cached_token', token);
|
| 254 |
-
|
| 255 |
closeTokenInput();
|
| 256 |
} else {
|
| 257 |
tokenError = `Invalid token (${response.status}). Please check your token and try again.`;
|
|
@@ -264,28 +271,32 @@
|
|
| 264 |
|
| 265 |
<div class="flex h-screen bg-white">
|
| 266 |
<!-- Sidebar -->
|
| 267 |
-
<aside
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
<div class="p-4 border-b border-gray-200">
|
| 269 |
<div class="flex items-center gap-3">
|
| 270 |
<img src="/assets/hf-studio-logo.png" alt="HF Logo" class="w-8 h-8" />
|
| 271 |
-
<h1 class="text-xl font-semibold">
|
|
|
|
|
|
|
| 272 |
</div>
|
| 273 |
</div>
|
| 274 |
-
|
| 275 |
-
<nav class="p-2 text-sm flex-1">
|
| 276 |
-
<div class="mt-2 mb-1 px-2 text-xs font-medium text-gray-500 uppercase">
|
| 277 |
-
|
| 278 |
-
</div>
|
| 279 |
-
|
| 280 |
<button
|
| 281 |
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-100 transition-colors text-left
|
| 282 |
{currentPage === 'tts' ? 'bg-gray-100' : ''}"
|
| 283 |
-
on:click={() => currentPage = 'tts'}
|
| 284 |
>
|
| 285 |
<span>ποΈ</span>
|
| 286 |
<span>Text to Speech</span>
|
| 287 |
</button>
|
| 288 |
-
|
| 289 |
<button
|
| 290 |
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
|
| 291 |
disabled
|
|
@@ -293,7 +304,7 @@
|
|
| 293 |
<span>π΅</span>
|
| 294 |
<span>Voice Cloning</span>
|
| 295 |
</button>
|
| 296 |
-
|
| 297 |
<button
|
| 298 |
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
|
| 299 |
disabled
|
|
@@ -301,7 +312,7 @@
|
|
| 301 |
<span>π§</span>
|
| 302 |
<span>Speech to Text</span>
|
| 303 |
</button>
|
| 304 |
-
|
| 305 |
<button
|
| 306 |
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
|
| 307 |
disabled
|
|
@@ -309,7 +320,7 @@
|
|
| 309 |
<span>πΌ</span>
|
| 310 |
<span>Sound Effects</span>
|
| 311 |
</button>
|
| 312 |
-
|
| 313 |
<button
|
| 314 |
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
|
| 315 |
disabled
|
|
@@ -317,7 +328,7 @@
|
|
| 317 |
<span>πΈ</span>
|
| 318 |
<span>Music Generation</span>
|
| 319 |
</button>
|
| 320 |
-
|
| 321 |
<button
|
| 322 |
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
|
| 323 |
disabled
|
|
@@ -325,9 +336,8 @@
|
|
| 325 |
<span>π</span>
|
| 326 |
<span>Audio Enhancement</span>
|
| 327 |
</button>
|
| 328 |
-
|
| 329 |
</nav>
|
| 330 |
-
|
| 331 |
<!-- Sign in with Hugging Face at bottom -->
|
| 332 |
<div class="p-2 relative">
|
| 333 |
<button
|
|
@@ -344,30 +354,39 @@
|
|
| 344 |
<span>Sign In</span>
|
| 345 |
{/if}
|
| 346 |
</button>
|
| 347 |
-
|
| 348 |
<!-- Sign In Popover -->
|
| 349 |
{#if showSignInPopover && !isLoggedIn}
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
</div>
|
| 360 |
-
</div>
|
| 361 |
-
<!-- Arrow pointing down -->
|
| 362 |
-
<div class="absolute top-full left-1/2 transform -translate-x-1/2">
|
| 363 |
-
<div class="w-0 h-0 border-l-4 border-r-4 border-t-4 border-l-transparent border-r-transparent border-t-blue-600"></div>
|
| 364 |
</div>
|
| 365 |
</div>
|
| 366 |
-
</div>
|
| 367 |
{/if}
|
| 368 |
</div>
|
| 369 |
</aside>
|
| 370 |
-
|
| 371 |
<!-- Main content -->
|
| 372 |
<main class="flex-1 overflow-auto">
|
| 373 |
<slot />
|
|
@@ -378,19 +397,24 @@
|
|
| 378 |
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
| 379 |
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4 shadow-xl">
|
| 380 |
<h2 class="text-xl font-semibold mb-4">Sign In with HuggingFace Token</h2>
|
| 381 |
-
|
| 382 |
<div class="mb-4 p-3 bg-blue-50 rounded-md text-sm">
|
| 383 |
<p class="text-blue-800 mb-2">
|
| 384 |
<strong>Manual Token Entry:</strong> Please enter your HuggingFace token.
|
| 385 |
</p>
|
| 386 |
<p class="text-blue-700">
|
| 387 |
-
1. Go to <a
|
| 388 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
3. Copy and paste it below
|
| 390 |
</p>
|
| 391 |
{#if isLocalEnvironment}
|
| 392 |
<p class="text-blue-600 mt-2">
|
| 393 |
-
<strong>Tip:</strong> You can also run <code>huggingface-cli login</code> in your terminal
|
|
|
|
| 394 |
</p>
|
| 395 |
{/if}
|
| 396 |
</div>
|
|
@@ -429,4 +453,4 @@
|
|
| 429 |
</div>
|
| 430 |
</div>
|
| 431 |
{/if}
|
| 432 |
-
</div>
|
|
|
|
| 2 |
import '../app.css';
|
| 3 |
import { Home, Settings, History, Github, Menu } from 'lucide-svelte';
|
| 4 |
import { onMount } from 'svelte';
|
| 5 |
+
|
| 6 |
let currentPage = 'tts';
|
| 7 |
let sidebarOpen = true;
|
| 8 |
+
|
| 9 |
// Initialize from cache immediately to avoid "Checking..." flash
|
| 10 |
const initToken = typeof window !== 'undefined' ? localStorage.getItem('hf_access_token') : null;
|
| 11 |
+
const initCachedToken =
|
| 12 |
+
typeof window !== 'undefined' ? localStorage.getItem('hf_cached_token') : null;
|
| 13 |
+
const initCachedUserInfo =
|
| 14 |
+
typeof window !== 'undefined' ? localStorage.getItem('hf_user_info') : null;
|
| 15 |
+
|
| 16 |
let isLoggedIn = false;
|
| 17 |
let username = '';
|
| 18 |
let isCheckingAuth = false; // Start as false by default
|
| 19 |
+
|
| 20 |
// Check if we have valid cached data
|
| 21 |
let hasCachedData = false;
|
| 22 |
if (initToken && initToken === initCachedToken && initCachedUserInfo) {
|
|
|
|
| 33 |
// We have a token but no valid cache, need to check
|
| 34 |
isCheckingAuth = true;
|
| 35 |
}
|
| 36 |
+
|
| 37 |
let showTokenInput = false;
|
| 38 |
let tokenInput = '';
|
| 39 |
let tokenError = '';
|
| 40 |
let isLocalEnvironment = false;
|
| 41 |
let showSignInPopover = false;
|
| 42 |
+
|
| 43 |
onMount(() => {
|
| 44 |
window.addEventListener('show-signin-popover', () => {
|
| 45 |
showSignInPopover = true;
|
|
|
|
| 47 |
showSignInPopover = false;
|
| 48 |
}, 4000);
|
| 49 |
});
|
| 50 |
+
|
| 51 |
// Only check if we don't already have valid cached info or need to verify
|
| 52 |
if (!hasCachedData && initToken) {
|
| 53 |
checkLocalTokenAvailability();
|
|
|
|
| 56 |
// No token at all, check for local availability
|
| 57 |
checkLocalTokenAvailability();
|
| 58 |
}
|
| 59 |
+
|
| 60 |
document.addEventListener('visibilitychange', () => {
|
| 61 |
if (!document.hidden) {
|
| 62 |
checkLoginStatus();
|
| 63 |
}
|
| 64 |
});
|
| 65 |
+
|
| 66 |
window.addEventListener('storage', checkLoginStatus);
|
| 67 |
+
|
| 68 |
const interval = setInterval(checkLoginStatus, 1000);
|
| 69 |
+
|
| 70 |
return () => {
|
| 71 |
window.removeEventListener('storage', checkLoginStatus);
|
| 72 |
clearInterval(interval);
|
| 73 |
};
|
| 74 |
});
|
| 75 |
+
|
| 76 |
async function checkLocalTokenAvailability() {
|
| 77 |
// Skip if we already have valid cached info
|
| 78 |
if (isLoggedIn && hasCachedData) {
|
| 79 |
return;
|
| 80 |
}
|
| 81 |
+
|
| 82 |
isCheckingAuth = true;
|
| 83 |
+
|
| 84 |
try {
|
| 85 |
const response = await fetch('/api/auth/local-token');
|
| 86 |
const data = await response.json();
|
| 87 |
+
|
| 88 |
if (data.available) {
|
| 89 |
isLocalEnvironment = true;
|
| 90 |
localStorage.setItem('hf_access_token', data.token);
|
| 91 |
+
|
| 92 |
+
if (data.user_info && data.user_info.name !== 'Local User') {
|
| 93 |
isLoggedIn = true;
|
| 94 |
username = data.user_info.name.split(' ')[0];
|
| 95 |
} else {
|
| 96 |
isLoggedIn = true;
|
| 97 |
+
username = 'Local User';
|
| 98 |
}
|
| 99 |
} else {
|
| 100 |
isLocalEnvironment = false;
|
|
|
|
| 110 |
const token = localStorage.getItem('hf_access_token');
|
| 111 |
const cachedUserInfo = localStorage.getItem('hf_user_info');
|
| 112 |
const cachedToken = localStorage.getItem('hf_cached_token');
|
| 113 |
+
|
| 114 |
if (token) {
|
| 115 |
// If token hasn't changed and we have cached info, use it
|
| 116 |
if (token === cachedToken && cachedUserInfo) {
|
|
|
|
| 135 |
localStorage.removeItem('hf_cached_token');
|
| 136 |
}
|
| 137 |
}
|
| 138 |
+
|
| 139 |
async function fetchUserInfo(token) {
|
| 140 |
isCheckingAuth = true;
|
| 141 |
+
|
| 142 |
try {
|
| 143 |
const response = await fetch('https://huggingface.co/api/whoami-v2', {
|
| 144 |
headers: {
|
| 145 |
+
Authorization: `Bearer ${token}`,
|
| 146 |
+
},
|
| 147 |
});
|
| 148 |
+
|
| 149 |
if (response.ok) {
|
| 150 |
const userData = await response.json();
|
| 151 |
isLoggedIn = true;
|
| 152 |
+
const fullName =
|
| 153 |
+
userData.name || userData.fullname || userData.login || userData.username || 'User';
|
| 154 |
username = fullName.split(' ')[0];
|
| 155 |
+
|
| 156 |
// Cache the user info and token
|
| 157 |
const userInfo = { username, fullName };
|
| 158 |
localStorage.setItem('hf_user_info', JSON.stringify(userInfo));
|
|
|
|
| 188 |
isCheckingAuth = false;
|
| 189 |
}
|
| 190 |
}
|
| 191 |
+
|
| 192 |
async function handleAuthAction() {
|
| 193 |
if (isLoggedIn) {
|
| 194 |
localStorage.removeItem('hf_access_token');
|
|
|
|
| 198 |
isLoggedIn = false;
|
| 199 |
username = '';
|
| 200 |
} else {
|
| 201 |
+
if (
|
| 202 |
+
window.location.hostname.includes('hf.space') ||
|
| 203 |
+
window.location.hostname.includes('huggingface.co')
|
| 204 |
+
) {
|
| 205 |
try {
|
| 206 |
const response = await fetch('/api/auth/oauth-config');
|
| 207 |
const config = await response.json();
|
|
|
|
| 241 |
try {
|
| 242 |
const response = await fetch('https://huggingface.co/api/whoami-v2', {
|
| 243 |
headers: {
|
| 244 |
+
Authorization: `Bearer ${tokenInput.trim()}`,
|
| 245 |
+
},
|
| 246 |
});
|
| 247 |
|
| 248 |
if (response.ok) {
|
|
|
|
| 250 |
const token = tokenInput.trim();
|
| 251 |
localStorage.setItem('hf_access_token', token);
|
| 252 |
isLoggedIn = true;
|
| 253 |
+
const fullName =
|
| 254 |
+
userData.name || userData.fullname || userData.login || userData.username || 'User';
|
| 255 |
username = fullName.split(' ')[0];
|
| 256 |
+
|
| 257 |
// Cache the user info and token
|
| 258 |
const userInfo = { username, fullName };
|
| 259 |
localStorage.setItem('hf_user_info', JSON.stringify(userInfo));
|
| 260 |
localStorage.setItem('hf_cached_token', token);
|
| 261 |
+
|
| 262 |
closeTokenInput();
|
| 263 |
} else {
|
| 264 |
tokenError = `Invalid token (${response.status}). Please check your token and try again.`;
|
|
|
|
| 271 |
|
| 272 |
<div class="flex h-screen bg-white">
|
| 273 |
<!-- Sidebar -->
|
| 274 |
+
<aside
|
| 275 |
+
class="w-56 border-r border-gray-200 bg-white flex-shrink-0 flex flex-col h-full {sidebarOpen
|
| 276 |
+
? ''
|
| 277 |
+
: 'hidden'}"
|
| 278 |
+
>
|
| 279 |
<div class="p-4 border-b border-gray-200">
|
| 280 |
<div class="flex items-center gap-3">
|
| 281 |
<img src="/assets/hf-studio-logo.png" alt="HF Logo" class="w-8 h-8" />
|
| 282 |
+
<h1 class="text-xl font-semibold">
|
| 283 |
+
HFStudio<sup class="text-xs text-gray-500 ml-1">BETA</sup>
|
| 284 |
+
</h1>
|
| 285 |
</div>
|
| 286 |
</div>
|
| 287 |
+
|
| 288 |
+
<nav class="p-2 text-sm flex-1">
|
| 289 |
+
<div class="mt-2 mb-1 px-2 text-xs font-medium text-gray-500 uppercase">Tasks</div>
|
| 290 |
+
|
|
|
|
|
|
|
| 291 |
<button
|
| 292 |
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-100 transition-colors text-left
|
| 293 |
{currentPage === 'tts' ? 'bg-gray-100' : ''}"
|
| 294 |
+
on:click={() => (currentPage = 'tts')}
|
| 295 |
>
|
| 296 |
<span>ποΈ</span>
|
| 297 |
<span>Text to Speech</span>
|
| 298 |
</button>
|
| 299 |
+
|
| 300 |
<button
|
| 301 |
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
|
| 302 |
disabled
|
|
|
|
| 304 |
<span>π΅</span>
|
| 305 |
<span>Voice Cloning</span>
|
| 306 |
</button>
|
| 307 |
+
|
| 308 |
<button
|
| 309 |
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
|
| 310 |
disabled
|
|
|
|
| 312 |
<span>π§</span>
|
| 313 |
<span>Speech to Text</span>
|
| 314 |
</button>
|
| 315 |
+
|
| 316 |
<button
|
| 317 |
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
|
| 318 |
disabled
|
|
|
|
| 320 |
<span>πΌ</span>
|
| 321 |
<span>Sound Effects</span>
|
| 322 |
</button>
|
| 323 |
+
|
| 324 |
<button
|
| 325 |
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
|
| 326 |
disabled
|
|
|
|
| 328 |
<span>πΈ</span>
|
| 329 |
<span>Music Generation</span>
|
| 330 |
</button>
|
| 331 |
+
|
| 332 |
<button
|
| 333 |
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
|
| 334 |
disabled
|
|
|
|
| 336 |
<span>π</span>
|
| 337 |
<span>Audio Enhancement</span>
|
| 338 |
</button>
|
|
|
|
| 339 |
</nav>
|
| 340 |
+
|
| 341 |
<!-- Sign in with Hugging Face at bottom -->
|
| 342 |
<div class="p-2 relative">
|
| 343 |
<button
|
|
|
|
| 354 |
<span>Sign In</span>
|
| 355 |
{/if}
|
| 356 |
</button>
|
| 357 |
+
|
| 358 |
<!-- Sign In Popover -->
|
| 359 |
{#if showSignInPopover && !isLoggedIn}
|
| 360 |
+
<div class="absolute bottom-full left-0 right-0 mb-2 z-50">
|
| 361 |
+
<div class="bg-blue-600 text-white text-sm rounded-lg p-3 shadow-lg relative">
|
| 362 |
+
<div class="flex items-start gap-2">
|
| 363 |
+
<svg class="w-4 h-4 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
| 364 |
+
<path
|
| 365 |
+
fill-rule="evenodd"
|
| 366 |
+
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
| 367 |
+
clip-rule="evenodd"
|
| 368 |
+
/>
|
| 369 |
+
</svg>
|
| 370 |
+
<div>
|
| 371 |
+
<p class="font-medium">Sign in required</p>
|
| 372 |
+
<p class="text-blue-100 text-xs mt-1">
|
| 373 |
+
You need to sign in to use HuggingFace Inference Providers for text-to-speech
|
| 374 |
+
generation.
|
| 375 |
+
</p>
|
| 376 |
+
</div>
|
| 377 |
+
</div>
|
| 378 |
+
<!-- Arrow pointing down -->
|
| 379 |
+
<div class="absolute top-full left-1/2 transform -translate-x-1/2">
|
| 380 |
+
<div
|
| 381 |
+
class="w-0 h-0 border-l-4 border-r-4 border-t-4 border-l-transparent border-r-transparent border-t-blue-600"
|
| 382 |
+
></div>
|
| 383 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
</div>
|
| 385 |
</div>
|
|
|
|
| 386 |
{/if}
|
| 387 |
</div>
|
| 388 |
</aside>
|
| 389 |
+
|
| 390 |
<!-- Main content -->
|
| 391 |
<main class="flex-1 overflow-auto">
|
| 392 |
<slot />
|
|
|
|
| 397 |
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
| 398 |
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4 shadow-xl">
|
| 399 |
<h2 class="text-xl font-semibold mb-4">Sign In with HuggingFace Token</h2>
|
| 400 |
+
|
| 401 |
<div class="mb-4 p-3 bg-blue-50 rounded-md text-sm">
|
| 402 |
<p class="text-blue-800 mb-2">
|
| 403 |
<strong>Manual Token Entry:</strong> Please enter your HuggingFace token.
|
| 404 |
</p>
|
| 405 |
<p class="text-blue-700">
|
| 406 |
+
1. Go to <a
|
| 407 |
+
href="https://huggingface.co/settings/tokens"
|
| 408 |
+
target="_blank"
|
| 409 |
+
class="underline text-blue-600">HuggingFace Settings</a
|
| 410 |
+
><br />
|
| 411 |
+
2. Create a new token with "Inference API" permissions<br />
|
| 412 |
3. Copy and paste it below
|
| 413 |
</p>
|
| 414 |
{#if isLocalEnvironment}
|
| 415 |
<p class="text-blue-600 mt-2">
|
| 416 |
+
<strong>Tip:</strong> You can also run <code>huggingface-cli login</code> in your terminal
|
| 417 |
+
to automatically use your local token.
|
| 418 |
</p>
|
| 419 |
{/if}
|
| 420 |
</div>
|
|
|
|
| 453 |
</div>
|
| 454 |
</div>
|
| 455 |
{/if}
|
| 456 |
+
</div>
|
frontend/src/routes/+page.svelte
CHANGED
|
@@ -1,10 +1,29 @@
|
|
| 1 |
<script>
|
| 2 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import { onMount } from 'svelte';
|
| 4 |
import Prism from 'prismjs';
|
| 5 |
import 'prismjs/components/prism-python';
|
| 6 |
import 'prismjs/components/prism-bash';
|
| 7 |
-
|
| 8 |
let text = `In a hole in the ground there lived a hobbit.`;
|
| 9 |
let selectedVoice = 'Lily';
|
| 10 |
let selectedModel = 'Chatterbox';
|
|
@@ -32,32 +51,52 @@
|
|
| 32 |
let errorMessage = '';
|
| 33 |
let errorDetails = '';
|
| 34 |
let currentUsername = null;
|
| 35 |
-
|
| 36 |
const models = [
|
| 37 |
{ id: 'chatterbox', name: 'Chatterbox', badge: 'recommended' },
|
| 38 |
{ id: 'kokoro', name: 'Kokoro', badge: 'coming soon', disabled: true },
|
| 39 |
];
|
| 40 |
-
|
| 41 |
const voices = [
|
| 42 |
-
{
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
];
|
| 47 |
-
|
| 48 |
async function generateSpeech() {
|
| 49 |
if (!text.trim()) return;
|
| 50 |
-
|
| 51 |
const accessToken = getAccessToken();
|
| 52 |
if (!accessToken) {
|
| 53 |
window.dispatchEvent(new CustomEvent('show-signin-popover'));
|
| 54 |
return;
|
| 55 |
}
|
| 56 |
-
|
| 57 |
isGenerating = true;
|
| 58 |
audioUrl = null;
|
| 59 |
currentTime = 0;
|
| 60 |
-
|
| 61 |
// Generate setup codes if not already present
|
| 62 |
if (!setupCode) {
|
| 63 |
setupCode = generateSetupCode();
|
|
@@ -65,9 +104,9 @@
|
|
| 65 |
if (!importCode) {
|
| 66 |
importCode = generateImportCode();
|
| 67 |
}
|
| 68 |
-
|
| 69 |
const ttsCode = generateTTSCode();
|
| 70 |
-
|
| 71 |
if (viewMode === 'ui') {
|
| 72 |
codeButtonFlash = true;
|
| 73 |
setTimeout(() => {
|
|
@@ -75,12 +114,12 @@
|
|
| 75 |
}, 2500);
|
| 76 |
}
|
| 77 |
isPlaying = false;
|
| 78 |
-
|
| 79 |
audioTitle = text.length > 30 ? text.substring(0, 30) + '...' : text;
|
| 80 |
-
|
| 81 |
try {
|
| 82 |
const accessToken = getAccessToken();
|
| 83 |
-
|
| 84 |
const requestBody = {
|
| 85 |
text: text,
|
| 86 |
voice_id: selectedVoice.toLowerCase(),
|
|
@@ -89,36 +128,36 @@
|
|
| 89 |
access_token: accessToken,
|
| 90 |
parameters: {
|
| 91 |
exaggeration: exaggeration,
|
| 92 |
-
temperature: temperature
|
| 93 |
-
}
|
| 94 |
};
|
| 95 |
-
|
| 96 |
const response = await fetch('/api/tts/generate', {
|
| 97 |
method: 'POST',
|
| 98 |
headers: {
|
| 99 |
'Content-Type': 'application/json',
|
| 100 |
},
|
| 101 |
-
body: JSON.stringify(requestBody)
|
| 102 |
});
|
| 103 |
-
|
| 104 |
if (!response.ok) {
|
| 105 |
const errorText = await response.text();
|
| 106 |
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
|
| 107 |
}
|
| 108 |
-
|
| 109 |
const result = await response.json();
|
| 110 |
-
|
| 111 |
if (result.success && result.audio_url) {
|
| 112 |
audioUrl = result.audio_url;
|
| 113 |
-
|
| 114 |
// Add to history with result
|
| 115 |
addCodeToHistory(ttsCode, {
|
| 116 |
type: 'audio',
|
| 117 |
url: result.audio_url,
|
| 118 |
title: audioTitle,
|
| 119 |
-
duration: result.duration
|
| 120 |
});
|
| 121 |
-
|
| 122 |
if (viewMode === 'ui') {
|
| 123 |
codeButtonFlash = true;
|
| 124 |
setTimeout(() => {
|
|
@@ -130,20 +169,22 @@
|
|
| 130 |
showError('Generation Failed', errorMessage);
|
| 131 |
audioUrl = null;
|
| 132 |
}
|
| 133 |
-
|
| 134 |
} catch (error) {
|
| 135 |
-
showError(
|
|
|
|
|
|
|
|
|
|
| 136 |
audioUrl = null;
|
| 137 |
} finally {
|
| 138 |
isGenerating = false;
|
| 139 |
}
|
| 140 |
}
|
| 141 |
-
|
| 142 |
function getAccessToken() {
|
| 143 |
if (typeof window !== 'undefined' && window.gradio && window.gradio.auth_token) {
|
| 144 |
return window.gradio.auth_token;
|
| 145 |
}
|
| 146 |
-
|
| 147 |
const metaToken = document.querySelector('meta[name="hf-oauth-token"]');
|
| 148 |
if (metaToken) {
|
| 149 |
const token = metaToken.getAttribute('content');
|
|
@@ -151,29 +192,29 @@
|
|
| 151 |
return token;
|
| 152 |
}
|
| 153 |
}
|
| 154 |
-
|
| 155 |
const possibleKeys = [
|
| 156 |
'hf_access_token',
|
| 157 |
-
'hf_token',
|
| 158 |
'huggingface_token',
|
| 159 |
'oauth_token',
|
| 160 |
-
'access_token'
|
| 161 |
];
|
| 162 |
-
|
| 163 |
for (const key of possibleKeys) {
|
| 164 |
const token = localStorage.getItem(key);
|
| 165 |
if (token) {
|
| 166 |
return token;
|
| 167 |
}
|
| 168 |
}
|
| 169 |
-
|
| 170 |
for (const key of possibleKeys) {
|
| 171 |
const token = sessionStorage.getItem(key);
|
| 172 |
if (token) {
|
| 173 |
return token;
|
| 174 |
}
|
| 175 |
}
|
| 176 |
-
|
| 177 |
const cookies = document.cookie.split(';');
|
| 178 |
for (const cookie of cookies) {
|
| 179 |
const [name, value] = cookie.trim().split('=');
|
|
@@ -181,7 +222,7 @@
|
|
| 181 |
return decodeURIComponent(value);
|
| 182 |
}
|
| 183 |
}
|
| 184 |
-
|
| 185 |
try {
|
| 186 |
const authHeader = document.querySelector('script[data-hf-token]');
|
| 187 |
if (authHeader) {
|
|
@@ -191,10 +232,10 @@
|
|
| 191 |
}
|
| 192 |
}
|
| 193 |
} catch (e) {}
|
| 194 |
-
|
| 195 |
return null;
|
| 196 |
}
|
| 197 |
-
|
| 198 |
function togglePlayPause() {
|
| 199 |
if (audioElement) {
|
| 200 |
if (isPlaying) {
|
|
@@ -204,33 +245,33 @@
|
|
| 204 |
}
|
| 205 |
}
|
| 206 |
}
|
| 207 |
-
|
| 208 |
function handleAudioLoad() {
|
| 209 |
if (audioElement) {
|
| 210 |
duration = audioElement.duration;
|
| 211 |
}
|
| 212 |
}
|
| 213 |
-
|
| 214 |
function handleTimeUpdate() {
|
| 215 |
if (audioElement) {
|
| 216 |
currentTime = audioElement.currentTime;
|
| 217 |
}
|
| 218 |
}
|
| 219 |
-
|
| 220 |
function handlePlay() {
|
| 221 |
isPlaying = true;
|
| 222 |
}
|
| 223 |
-
|
| 224 |
function handlePause() {
|
| 225 |
isPlaying = false;
|
| 226 |
}
|
| 227 |
-
|
| 228 |
function formatTime(seconds) {
|
| 229 |
const mins = Math.floor(seconds / 60);
|
| 230 |
const secs = Math.floor(seconds % 60);
|
| 231 |
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
| 232 |
}
|
| 233 |
-
|
| 234 |
function downloadAudio() {
|
| 235 |
if (audioUrl) {
|
| 236 |
const a = document.createElement('a');
|
|
@@ -239,13 +280,12 @@
|
|
| 239 |
a.click();
|
| 240 |
}
|
| 241 |
}
|
| 242 |
-
|
| 243 |
-
function shareAudio() {
|
| 244 |
-
|
| 245 |
-
|
| 246 |
function playSampleVoice(voice, event) {
|
| 247 |
event.stopPropagation();
|
| 248 |
-
|
| 249 |
if (playingSampleVoice === voice.name) {
|
| 250 |
if (sampleAudioElement) {
|
| 251 |
sampleAudioElement.pause();
|
|
@@ -257,9 +297,9 @@
|
|
| 257 |
sampleAudioElement.pause();
|
| 258 |
}
|
| 259 |
playingSampleVoice = voice.name;
|
| 260 |
-
|
| 261 |
const sampleUrl = voice.sample || '/samples/harvard.wav';
|
| 262 |
-
|
| 263 |
if (!sampleAudioElement) {
|
| 264 |
sampleAudioElement = new Audio(sampleUrl);
|
| 265 |
sampleAudioElement.addEventListener('ended', () => {
|
|
@@ -268,20 +308,20 @@
|
|
| 268 |
} else {
|
| 269 |
sampleAudioElement.src = sampleUrl;
|
| 270 |
}
|
| 271 |
-
|
| 272 |
-
sampleAudioElement.play().catch(err => {
|
| 273 |
playingSampleVoice = null;
|
| 274 |
});
|
| 275 |
}
|
| 276 |
}
|
| 277 |
-
|
| 278 |
function handleKeyDown(event) {
|
| 279 |
if (event.key === 'Enter' && !event.shiftKey) {
|
| 280 |
event.preventDefault();
|
| 281 |
generateSpeech();
|
| 282 |
}
|
| 283 |
}
|
| 284 |
-
|
| 285 |
function handleClickOutside(event) {
|
| 286 |
if (!event.target.closest('.model-dropdown')) {
|
| 287 |
modelDropdownOpen = false;
|
|
@@ -292,13 +332,13 @@
|
|
| 292 |
const entry = {
|
| 293 |
id: Date.now() + Math.random(),
|
| 294 |
code,
|
| 295 |
-
result
|
| 296 |
};
|
| 297 |
codeHistory = [...codeHistory, entry];
|
| 298 |
saveHistoryToStorage();
|
| 299 |
return entry;
|
| 300 |
}
|
| 301 |
-
|
| 302 |
function saveHistoryToStorage() {
|
| 303 |
if (!currentUsername) return;
|
| 304 |
const storageKey = `hfstudio_history_${currentUsername}`;
|
|
@@ -306,11 +346,11 @@
|
|
| 306 |
username: currentUsername,
|
| 307 |
setupCode,
|
| 308 |
importCode,
|
| 309 |
-
history: codeHistory
|
| 310 |
};
|
| 311 |
localStorage.setItem(storageKey, JSON.stringify(historyData));
|
| 312 |
}
|
| 313 |
-
|
| 314 |
function loadHistoryFromStorage() {
|
| 315 |
if (!currentUsername) return;
|
| 316 |
const storageKey = `hfstudio_history_${currentUsername}`;
|
|
@@ -328,7 +368,7 @@
|
|
| 328 |
}
|
| 329 |
}
|
| 330 |
}
|
| 331 |
-
|
| 332 |
function resetHistory() {
|
| 333 |
codeHistory = [];
|
| 334 |
setupCode = null;
|
|
@@ -348,7 +388,7 @@ hfstudio start ${selectedModel.toLowerCase()} --port 7861`;
|
|
| 348 |
pip install huggingface-hub`;
|
| 349 |
}
|
| 350 |
}
|
| 351 |
-
|
| 352 |
function generateImportCode() {
|
| 353 |
if (mode === 'local') {
|
| 354 |
return `from huggingface_hub import InferenceClient
|
|
@@ -371,12 +411,12 @@ client = InferenceClient(
|
|
| 371 |
|
| 372 |
function generateTTSCode() {
|
| 373 |
const voiceUrls = {
|
| 374 |
-
lily:
|
| 375 |
-
andrew:
|
| 376 |
-
fairy:
|
| 377 |
-
pirate:
|
| 378 |
};
|
| 379 |
-
|
| 380 |
if (mode === 'local') {
|
| 381 |
return `# Generate speech
|
| 382 |
text = """${text}"""
|
|
@@ -464,9 +504,9 @@ print(f"β Audio saved to {output_filename}")
|
|
| 464 |
fetchUserInfo(token);
|
| 465 |
}
|
| 466 |
};
|
| 467 |
-
|
| 468 |
checkUsername();
|
| 469 |
-
|
| 470 |
// Listen for auth changes
|
| 471 |
window.addEventListener('storage', (e) => {
|
| 472 |
if (e.key === 'hf_access_token') {
|
|
@@ -474,32 +514,32 @@ print(f"β Audio saved to {output_filename}")
|
|
| 474 |
}
|
| 475 |
});
|
| 476 |
});
|
| 477 |
-
|
| 478 |
async function fetchUserInfo(token) {
|
| 479 |
try {
|
| 480 |
const response = await fetch('https://huggingface.co/api/whoami-v2', {
|
| 481 |
headers: {
|
| 482 |
-
|
| 483 |
-
}
|
| 484 |
});
|
| 485 |
-
|
| 486 |
if (response.ok) {
|
| 487 |
const userData = await response.json();
|
| 488 |
-
currentUsername =
|
|
|
|
| 489 |
loadHistoryFromStorage();
|
| 490 |
}
|
| 491 |
} catch (error) {
|
| 492 |
console.error('Error fetching user info:', error);
|
| 493 |
}
|
| 494 |
}
|
| 495 |
-
|
| 496 |
// Update setup/import codes when mode changes
|
| 497 |
$: if (mode) {
|
| 498 |
setupCode = generateSetupCode();
|
| 499 |
importCode = generateImportCode();
|
| 500 |
}
|
| 501 |
-
|
| 502 |
-
|
| 503 |
function toggleHistoryAudio(entry) {
|
| 504 |
if (!entry.audioElement) {
|
| 505 |
// Create audio element if it doesn't exist
|
|
@@ -509,13 +549,13 @@ print(f"β Audio saved to {output_filename}")
|
|
| 509 |
codeHistory = [...codeHistory]; // Trigger reactivity
|
| 510 |
});
|
| 511 |
}
|
| 512 |
-
|
| 513 |
if (entry.isPlaying) {
|
| 514 |
entry.audioElement.pause();
|
| 515 |
entry.isPlaying = false;
|
| 516 |
} else {
|
| 517 |
// Pause any other playing audio
|
| 518 |
-
codeHistory.forEach(e => {
|
| 519 |
if (e !== entry && e.isPlaying && e.audioElement) {
|
| 520 |
e.audioElement.pause();
|
| 521 |
e.isPlaying = false;
|
|
@@ -526,7 +566,7 @@ print(f"β Audio saved to {output_filename}")
|
|
| 526 |
}
|
| 527 |
codeHistory = [...codeHistory]; // Trigger reactivity
|
| 528 |
}
|
| 529 |
-
|
| 530 |
function downloadHistoryAudio(url, title) {
|
| 531 |
const link = document.createElement('a');
|
| 532 |
link.href = url;
|
|
@@ -535,14 +575,13 @@ print(f"β Audio saved to {output_filename}")
|
|
| 535 |
link.click();
|
| 536 |
document.body.removeChild(link);
|
| 537 |
}
|
| 538 |
-
|
| 539 |
function formatDuration(seconds) {
|
| 540 |
if (!seconds) return '0:00';
|
| 541 |
const mins = Math.floor(seconds / 60);
|
| 542 |
const secs = Math.floor(seconds % 60);
|
| 543 |
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
| 544 |
}
|
| 545 |
-
|
| 546 |
</script>
|
| 547 |
|
| 548 |
<div class="flex flex-col h-full" on:click={handleClickOutside}>
|
|
@@ -553,596 +592,659 @@ print(f"β Audio saved to {output_filename}")
|
|
| 553 |
<!-- View mode toggle -->
|
| 554 |
<div class="flex items-center bg-gray-100 rounded-md p-0.5">
|
| 555 |
<button
|
| 556 |
-
class="flex items-center gap-1.5 px-3 py-1 text-sm font-medium rounded transition-colors {viewMode ===
|
| 557 |
-
|
|
|
|
|
|
|
|
|
|
| 558 |
>
|
| 559 |
<Layout size={14} />
|
| 560 |
UI
|
| 561 |
</button>
|
| 562 |
<button
|
| 563 |
-
class="flex items-center gap-1.5 px-3 py-1 text-sm font-medium rounded transition-colors relative overflow-hidden {viewMode ===
|
| 564 |
-
|
|
|
|
|
|
|
|
|
|
| 565 |
>
|
| 566 |
<Code size={14} />
|
| 567 |
Code Recorder
|
| 568 |
{#if codeButtonFlash}
|
| 569 |
-
|
| 570 |
{/if}
|
| 571 |
</button>
|
| 572 |
</div>
|
| 573 |
</div>
|
| 574 |
</div>
|
| 575 |
</header>
|
| 576 |
-
|
| 577 |
<!-- Main content area -->
|
| 578 |
{#if viewMode === 'ui'}
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
/>
|
| 590 |
-
|
| 591 |
-
</div>
|
| 592 |
-
|
| 593 |
-
<!-- Fixed bottom generate button -->
|
| 594 |
-
<div class="fixed bottom-0 left-56 right-80 p-4 bg-white border-t border-gray-200">
|
| 595 |
-
<div class="flex items-center justify-between mb-3">
|
| 596 |
-
<span class="text-sm text-gray-500">{text.length} / 5,000 characters</span>
|
| 597 |
-
</div>
|
| 598 |
-
<button
|
| 599 |
-
on:click={generateSpeech}
|
| 600 |
-
disabled={isGenerating || !text.trim()}
|
| 601 |
-
class="w-full px-6 py-3 bg-gradient-to-r from-amber-400 to-orange-500 text-white rounded-lg font-medium hover:from-amber-500 hover:to-orange-600 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 shadow-sm"
|
| 602 |
-
>
|
| 603 |
-
{#if isGenerating}
|
| 604 |
-
<Loader2 size={20} class="animate-spin" />
|
| 605 |
-
Generating...
|
| 606 |
-
{:else}
|
| 607 |
-
<Play size={20} />
|
| 608 |
-
Generate speech
|
| 609 |
-
{/if}
|
| 610 |
-
</button>
|
| 611 |
-
</div>
|
| 612 |
-
|
| 613 |
-
<!-- Generated audio section -->
|
| 614 |
-
{#if audioUrl}
|
| 615 |
-
<div class="p-4 border border-gray-200 rounded-lg bg-white">
|
| 616 |
-
<!-- Audio title and voice info -->
|
| 617 |
-
<div class="flex items-center gap-3 mb-4">
|
| 618 |
-
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
| 619 |
-
<div class="flex-1">
|
| 620 |
-
<h3 class="font-medium text-gray-900 text-sm">{audioTitle}</h3>
|
| 621 |
-
<p class="text-xs text-gray-500">{selectedVoice} β’ Created 1 second ago</p>
|
| 622 |
-
</div>
|
| 623 |
-
<!-- Mini action buttons -->
|
| 624 |
-
<div class="flex items-center gap-2">
|
| 625 |
-
<button
|
| 626 |
-
on:click={shareAudio}
|
| 627 |
-
class="flex items-center gap-1.5 px-3 py-1.5 text-sm border border-gray-200 rounded-md hover:bg-gray-50 transition-colors"
|
| 628 |
-
>
|
| 629 |
-
<Share size={14} class="text-gray-600" />
|
| 630 |
-
<span class="text-gray-700">Share</span>
|
| 631 |
-
</button>
|
| 632 |
-
<button
|
| 633 |
-
on:click={downloadAudio}
|
| 634 |
-
class="flex items-center gap-1.5 px-3 py-1.5 text-sm border border-gray-200 rounded-md hover:bg-gray-50 transition-colors"
|
| 635 |
-
>
|
| 636 |
-
<span class="text-gray-700">Download</span>
|
| 637 |
-
<Download size={14} class="text-gray-600" />
|
| 638 |
-
</button>
|
| 639 |
-
</div>
|
| 640 |
</div>
|
| 641 |
-
|
| 642 |
-
<!--
|
| 643 |
-
<div class="
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
on:click={togglePlayPause}
|
| 647 |
-
class="w-8 h-8 bg-black rounded-full flex items-center justify-center hover:bg-gray-800 transition-colors"
|
| 648 |
-
>
|
| 649 |
-
{#if isPlaying}
|
| 650 |
-
<div class="pause-filled text-white"></div>
|
| 651 |
-
{:else}
|
| 652 |
-
<Play size={14} class="text-white ml-0.5" />
|
| 653 |
-
{/if}
|
| 654 |
-
</button>
|
| 655 |
-
|
| 656 |
-
<!-- Progress bar -->
|
| 657 |
-
<div class="flex-1 flex items-center gap-2">
|
| 658 |
-
<span class="text-xs text-gray-500 font-mono">{formatTime(currentTime)}</span>
|
| 659 |
-
<div class="flex-1 h-1 bg-gray-200 rounded-full cursor-pointer">
|
| 660 |
-
<div
|
| 661 |
-
class="h-full bg-gradient-to-r from-amber-400 to-orange-500 rounded-full transition-all"
|
| 662 |
-
style="width: {(currentTime / duration) * 100}%"
|
| 663 |
-
></div>
|
| 664 |
-
</div>
|
| 665 |
-
<span class="text-xs text-gray-500 font-mono">{formatTime(duration)}</span>
|
| 666 |
</div>
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
<!-- Skip back button -->
|
| 672 |
-
<button class="p-2 hover:bg-gray-100 rounded-full" title="Skip back">
|
| 673 |
-
<SkipBack size={20} class="text-gray-600" />
|
| 674 |
-
</button>
|
| 675 |
-
|
| 676 |
-
<!-- Play/Pause button -->
|
| 677 |
-
<button
|
| 678 |
-
on:click={togglePlayPause}
|
| 679 |
-
class="w-12 h-12 bg-black rounded-full flex items-center justify-center hover:bg-gray-800 transition-colors"
|
| 680 |
>
|
| 681 |
-
{#if
|
| 682 |
-
<
|
|
|
|
| 683 |
{:else}
|
| 684 |
-
<Play size={20}
|
|
|
|
| 685 |
{/if}
|
| 686 |
</button>
|
| 687 |
-
|
| 688 |
-
<!-- Skip forward button -->
|
| 689 |
-
<button class="p-2 hover:bg-gray-100 rounded-full" title="Skip forward">
|
| 690 |
-
<SkipForward size={20} class="text-gray-600" />
|
| 691 |
-
</button>
|
| 692 |
-
|
| 693 |
-
<!-- Progress bar -->
|
| 694 |
-
<div class="flex-1 flex items-center gap-3">
|
| 695 |
-
<span class="text-xs text-gray-500 font-mono">{formatTime(currentTime)}</span>
|
| 696 |
-
<div class="flex-1 h-1 bg-gray-200 rounded-full">
|
| 697 |
-
<div
|
| 698 |
-
class="h-full bg-gradient-to-r from-amber-400 to-orange-500 rounded-full transition-all"
|
| 699 |
-
style="width: {(currentTime / duration) * 100}%"
|
| 700 |
-
></div>
|
| 701 |
-
</div>
|
| 702 |
-
<span class="text-xs text-gray-500 font-mono">{formatTime(duration)}</span>
|
| 703 |
-
</div>
|
| 704 |
-
|
| 705 |
-
<!-- Action buttons -->
|
| 706 |
-
<div class="flex items-center gap-2">
|
| 707 |
-
<button
|
| 708 |
-
on:click={shareAudio}
|
| 709 |
-
class="flex items-center gap-2 px-3 py-1.5 text-sm border border-gray-200 rounded-md hover:bg-gray-50"
|
| 710 |
-
>
|
| 711 |
-
<Share size={14} />
|
| 712 |
-
Share
|
| 713 |
-
</button>
|
| 714 |
-
<button
|
| 715 |
-
on:click={downloadAudio}
|
| 716 |
-
class="p-2 hover:bg-gray-100 rounded-md"
|
| 717 |
-
title="Download"
|
| 718 |
-
>
|
| 719 |
-
<Download size={16} class="text-gray-600" />
|
| 720 |
-
</button>
|
| 721 |
-
<button class="p-2 hover:bg-gray-100 rounded-md" title="More options">
|
| 722 |
-
<MoreHorizontal size={16} class="text-gray-600" />
|
| 723 |
-
</button>
|
| 724 |
-
</div>
|
| 725 |
</div>
|
| 726 |
-
|
| 727 |
-
<!--
|
| 728 |
{#if audioUrl}
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
style="display: none;"
|
| 737 |
-
/>
|
| 738 |
-
{/if}
|
| 739 |
-
</div>
|
| 740 |
-
{/if}
|
| 741 |
-
</div>
|
| 742 |
-
|
| 743 |
-
<!-- Right panel -->
|
| 744 |
-
<div class="w-80 border-l border-gray-200 bg-white p-4 overflow-y-auto">
|
| 745 |
-
<!-- Model selector -->
|
| 746 |
-
<div class="mb-6 relative model-dropdown">
|
| 747 |
-
<h3 class="font-medium text-gray-900 mb-3">Model</h3>
|
| 748 |
-
<button
|
| 749 |
-
on:click={() => modelDropdownOpen = !modelDropdownOpen}
|
| 750 |
-
class="w-full p-3 border border-gray-200 rounded-lg bg-white text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 focus:border-transparent appearance-none bg-no-repeat bg-right pr-10 shadow-sm text-left flex items-center justify-between"
|
| 751 |
-
>
|
| 752 |
-
<span>
|
| 753 |
-
{#each models as model}
|
| 754 |
-
{#if model.name === selectedModel}
|
| 755 |
-
{model.name}{#if model.badge} <span class="text-xs text-gray-500">({model.badge})</span>{/if}
|
| 756 |
-
{/if}
|
| 757 |
-
{/each}
|
| 758 |
-
</span>
|
| 759 |
-
<ChevronDown size={16} class="text-gray-500" />
|
| 760 |
-
</button>
|
| 761 |
-
|
| 762 |
-
{#if modelDropdownOpen}
|
| 763 |
-
<div class="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-10">
|
| 764 |
-
{#each models as model}
|
| 765 |
-
<button
|
| 766 |
-
class="w-full px-3 py-2 text-left transition-colors text-sm {model.disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'} {model.name === selectedModel ? 'bg-gray-100' : ''}"
|
| 767 |
-
disabled={model.disabled}
|
| 768 |
-
on:click={() => {
|
| 769 |
-
if (!model.disabled) {
|
| 770 |
-
selectedModel = model.name;
|
| 771 |
-
modelDropdownOpen = false;
|
| 772 |
-
}
|
| 773 |
-
}}
|
| 774 |
-
>
|
| 775 |
-
{model.name}{#if model.badge} <span class="text-xs text-gray-500">({model.badge})</span>{/if}
|
| 776 |
-
</button>
|
| 777 |
-
{/each}
|
| 778 |
-
</div>
|
| 779 |
-
{/if}
|
| 780 |
-
|
| 781 |
-
<!-- Pricing info -->
|
| 782 |
-
<div class="mt-2 text-xs text-gray-500">
|
| 783 |
-
Estimated $0.025 per 1000 characters ⒠<a href="https://huggingface.co/settings/billing" target="_blank" class="text-amber-600 hover:text-amber-700 underline">Billing ‴</a>
|
| 784 |
-
</div>
|
| 785 |
-
</div>
|
| 786 |
-
|
| 787 |
-
<div class="mb-6">
|
| 788 |
-
<div class="mb-3">
|
| 789 |
-
<h3 class="font-medium text-gray-900">Voice</h3>
|
| 790 |
-
</div>
|
| 791 |
-
|
| 792 |
-
<div class="space-y-2">
|
| 793 |
-
{#each voices as voice}
|
| 794 |
-
<button
|
| 795 |
-
class="w-full flex items-center justify-between p-2 rounded-lg hover:bg-gray-50 transition-colors text-left group border border-transparent
|
| 796 |
-
{voice.name === selectedVoice ? 'bg-gray-100 border-gray-200' : ''}"
|
| 797 |
-
on:click={() => selectedVoice = voice.name}
|
| 798 |
-
>
|
| 799 |
-
<div class="flex items-center gap-3 flex-1 min-w-0">
|
| 800 |
-
<div class="w-10 h-10 bg-gradient-to-br from-amber-400 to-orange-500 rounded-full flex items-center justify-center text-white text-sm font-semibold flex-shrink-0">
|
| 801 |
-
{voice.name[0]}
|
| 802 |
</div>
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
<
|
| 806 |
-
{
|
| 807 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 808 |
</div>
|
| 809 |
</div>
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
|
|
|
|
|
|
|
|
|
| 832 |
</div>
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 838 |
</div>
|
| 839 |
</div>
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
id="exaggeration-slider"
|
| 853 |
-
type="range"
|
| 854 |
-
bind:value={exaggeration}
|
| 855 |
-
min="0"
|
| 856 |
-
max="1"
|
| 857 |
-
step="0.01"
|
| 858 |
-
class="w-full h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-hf"
|
| 859 |
-
/>
|
| 860 |
-
<div class="flex justify-between text-xs text-gray-400 mt-1">
|
| 861 |
-
<span>None</span>
|
| 862 |
-
<span>Exaggerated</span>
|
| 863 |
-
</div>
|
| 864 |
-
</div>
|
| 865 |
-
|
| 866 |
-
<!-- Stability control -->
|
| 867 |
-
<div>
|
| 868 |
-
<div class="flex justify-between mb-1">
|
| 869 |
-
<label for="temperature-slider" class="text-sm font-medium text-gray-700">Stability</label>
|
| 870 |
-
<span class="text-sm text-gray-500">{temperature.toFixed(2)}</span>
|
| 871 |
-
</div>
|
| 872 |
-
<input
|
| 873 |
-
id="temperature-slider"
|
| 874 |
-
type="range"
|
| 875 |
-
bind:value={temperature}
|
| 876 |
-
min="0"
|
| 877 |
-
max="1"
|
| 878 |
-
step="0.01"
|
| 879 |
-
class="w-full h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-hf"
|
| 880 |
-
/>
|
| 881 |
-
<div class="flex justify-between text-xs text-gray-400 mt-1">
|
| 882 |
-
<span>More stable</span>
|
| 883 |
-
<span>More variable</span>
|
| 884 |
-
</div>
|
| 885 |
-
</div>
|
| 886 |
-
</div>
|
| 887 |
-
</div>
|
| 888 |
-
</div>
|
| 889 |
-
{:else}
|
| 890 |
-
<!-- Code view -->
|
| 891 |
-
<div class="flex-1 bg-gray-50 overflow-y-auto">
|
| 892 |
-
<div class="max-w-4xl mx-auto p-8">
|
| 893 |
-
<!-- Header -->
|
| 894 |
-
<div class="mb-6">
|
| 895 |
-
<div>
|
| 896 |
-
<h2 class="text-2xl font-semibold text-gray-900">Integration Code</h2>
|
| 897 |
-
<p class="text-sm text-gray-600 mt-1">
|
| 898 |
-
{#if mode === 'local'}
|
| 899 |
-
Python code to reproduce your actions using a local HFStudio server
|
| 900 |
-
{:else}
|
| 901 |
-
Python code to reproduce your actions via the API
|
| 902 |
{/if}
|
| 903 |
-
</p>
|
| 904 |
-
</div>
|
| 905 |
-
|
| 906 |
-
<!-- Toggle and Copy All button row -->
|
| 907 |
-
<div class="flex items-center justify-between mt-4">
|
| 908 |
-
<!-- API/Local Mode Toggle -->
|
| 909 |
-
<div class="flex items-center bg-gray-100 rounded-md p-0.5">
|
| 910 |
-
<button
|
| 911 |
-
class="px-3 py-1 text-sm font-medium rounded transition-colors {mode === 'api' ? 'bg-white shadow-sm' : 'text-gray-600'}"
|
| 912 |
-
on:click={() => mode = 'api'}
|
| 913 |
-
>
|
| 914 |
-
API
|
| 915 |
-
</button>
|
| 916 |
-
<button
|
| 917 |
-
class="px-3 py-1 text-sm font-medium rounded transition-colors {mode === 'local' ? 'bg-white shadow-sm' : 'text-gray-600'}"
|
| 918 |
-
on:click={() => mode = 'local'}
|
| 919 |
-
>
|
| 920 |
-
Local
|
| 921 |
-
</button>
|
| 922 |
</div>
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 930 |
>
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 937 |
>
|
| 938 |
-
<Copy size={16} class="text-gray-600" />
|
| 939 |
-
<span class="ml-2 text-sm font-medium text-gray-600">Copy All</span>
|
| 940 |
-
</button>
|
| 941 |
</div>
|
| 942 |
-
{/if}
|
| 943 |
</div>
|
| 944 |
-
</div>
|
| 945 |
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 964 |
<button
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
title="Copy setup code"
|
| 968 |
>
|
| 969 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 970 |
</button>
|
| 971 |
</div>
|
| 972 |
-
<div class="relative">
|
| 973 |
-
<pre class="p-4 overflow-x-auto bg-gray-50"><code class="language-bash text-sm">{@html Prism.highlight(setupCode, Prism.languages.bash, 'bash')}</code></pre>
|
| 974 |
-
</div>
|
| 975 |
</div>
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
<span class="text-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 985 |
</div>
|
| 986 |
-
<button
|
| 987 |
-
on:click={() => copyToClipboard(importCode)}
|
| 988 |
-
class="p-1.5 hover:bg-blue-100 rounded transition-colors"
|
| 989 |
-
title="Copy import code"
|
| 990 |
-
>
|
| 991 |
-
<Copy size={14} class="text-blue-600" />
|
| 992 |
-
</button>
|
| 993 |
</div>
|
| 994 |
-
|
| 995 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 996 |
</div>
|
| 997 |
</div>
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1007 |
<button
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
|
|
|
| 1011 |
>
|
| 1012 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1013 |
</button>
|
| 1014 |
</div>
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1018 |
</div>
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1026 |
<button
|
| 1027 |
-
on:click={() =>
|
| 1028 |
-
class="
|
|
|
|
| 1029 |
>
|
| 1030 |
-
{
|
| 1031 |
-
<Pause size={18} />
|
| 1032 |
-
{:else}
|
| 1033 |
-
<Play size={18} class="ml-0.5" />
|
| 1034 |
-
{/if}
|
| 1035 |
</button>
|
| 1036 |
-
<div class="flex-1">
|
| 1037 |
-
<div class="text-sm font-medium text-gray-900 truncate">{entry.result.title || 'Generated Audio'}</div>
|
| 1038 |
-
<div class="text-xs text-gray-500">Duration: {formatDuration(entry.result.duration || 0)}</div>
|
| 1039 |
-
</div>
|
| 1040 |
</div>
|
| 1041 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1042 |
<button
|
| 1043 |
-
on:click={() =>
|
| 1044 |
-
class="p-
|
| 1045 |
-
title="
|
| 1046 |
>
|
| 1047 |
-
<
|
| 1048 |
</button>
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1052 |
>
|
| 1053 |
-
<
|
| 1054 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1055 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1056 |
</div>
|
| 1057 |
-
|
| 1058 |
-
bind:this={entry.audioElement}
|
| 1059 |
-
src={entry.result.url}
|
| 1060 |
-
on:ended={() => entry.isPlaying = false}
|
| 1061 |
-
class="hidden"
|
| 1062 |
-
/>
|
| 1063 |
-
</div>
|
| 1064 |
</div>
|
| 1065 |
-
|
| 1066 |
-
</div>
|
| 1067 |
-
{/each}
|
| 1068 |
</div>
|
| 1069 |
-
{/if}
|
| 1070 |
</div>
|
| 1071 |
-
</div>
|
| 1072 |
{/if}
|
| 1073 |
-
|
| 1074 |
<!-- Copy notification toast -->
|
| 1075 |
{#if copyNotification}
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
|
|
|
|
|
|
| 1079 |
{/if}
|
| 1080 |
|
| 1081 |
<!-- Error Modal -->
|
| 1082 |
{#if showErrorModal}
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1094 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1095 |
</div>
|
| 1096 |
-
<button
|
| 1097 |
-
on:click={closeErrorModal}
|
| 1098 |
-
class="p-2 hover:bg-red-100 rounded-full transition-colors flex-shrink-0"
|
| 1099 |
-
title="Close"
|
| 1100 |
-
>
|
| 1101 |
-
<X size={20} class="text-gray-500" />
|
| 1102 |
-
</button>
|
| 1103 |
-
</div>
|
| 1104 |
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
|
|
|
|
|
|
|
|
|
| 1111 |
</div>
|
| 1112 |
-
{/if}
|
| 1113 |
-
</div>
|
| 1114 |
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
on:click={copyErrorMessage}
|
| 1119 |
-
class="flex items-center gap-2 px-4 py-2 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors"
|
| 1120 |
>
|
| 1121 |
-
<
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1130 |
</div>
|
| 1131 |
</div>
|
| 1132 |
-
</div>
|
| 1133 |
{/if}
|
| 1134 |
</div>
|
| 1135 |
|
| 1136 |
<style>
|
| 1137 |
@keyframes fade-in {
|
| 1138 |
-
from {
|
| 1139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1140 |
}
|
| 1141 |
-
|
| 1142 |
.animate-fade-in {
|
| 1143 |
animation: fade-in 0.3s ease-out;
|
| 1144 |
}
|
| 1145 |
-
|
| 1146 |
@keyframes sweep {
|
| 1147 |
0% {
|
| 1148 |
left: -100%;
|
|
@@ -1157,27 +1259,29 @@ print(f"β Audio saved to {output_filename}")
|
|
| 1157 |
left: 100%;
|
| 1158 |
}
|
| 1159 |
}
|
| 1160 |
-
|
| 1161 |
.flash-sweep {
|
| 1162 |
position: absolute;
|
| 1163 |
top: 0;
|
| 1164 |
left: -100%;
|
| 1165 |
width: 100%;
|
| 1166 |
height: 100%;
|
| 1167 |
-
background: linear-gradient(
|
| 1168 |
-
|
|
|
|
| 1169 |
rgba(251, 191, 36, 0.5) 25%,
|
| 1170 |
rgba(249, 115, 22, 0.8) 50%,
|
| 1171 |
rgba(251, 191, 36, 0.5) 75%,
|
| 1172 |
-
transparent 100%
|
|
|
|
| 1173 |
animation: sweep 2s ease-in-out;
|
| 1174 |
pointer-events: none;
|
| 1175 |
}
|
| 1176 |
-
|
| 1177 |
.code-flash {
|
| 1178 |
animation: pulse 0.5s ease-out;
|
| 1179 |
}
|
| 1180 |
-
|
| 1181 |
@keyframes pulse {
|
| 1182 |
0% {
|
| 1183 |
box-shadow: 0 0 0 0 rgba(251, 191, 36, 0);
|
|
@@ -1189,5 +1293,4 @@ print(f"β Audio saved to {output_filename}")
|
|
| 1189 |
box-shadow: 0 0 0 0 rgba(251, 191, 36, 0);
|
| 1190 |
}
|
| 1191 |
}
|
| 1192 |
-
|
| 1193 |
-
</style>
|
|
|
|
| 1 |
<script>
|
| 2 |
+
import {
|
| 3 |
+
Play,
|
| 4 |
+
Download,
|
| 5 |
+
Loader2,
|
| 6 |
+
AlertCircle,
|
| 7 |
+
ChevronDown,
|
| 8 |
+
Copy,
|
| 9 |
+
RefreshCw,
|
| 10 |
+
Share,
|
| 11 |
+
MoreHorizontal,
|
| 12 |
+
Settings,
|
| 13 |
+
Sliders,
|
| 14 |
+
Pause,
|
| 15 |
+
SkipBack,
|
| 16 |
+
SkipForward,
|
| 17 |
+
Layout,
|
| 18 |
+
Code,
|
| 19 |
+
X,
|
| 20 |
+
RotateCcw,
|
| 21 |
+
} from 'lucide-svelte';
|
| 22 |
import { onMount } from 'svelte';
|
| 23 |
import Prism from 'prismjs';
|
| 24 |
import 'prismjs/components/prism-python';
|
| 25 |
import 'prismjs/components/prism-bash';
|
| 26 |
+
|
| 27 |
let text = `In a hole in the ground there lived a hobbit.`;
|
| 28 |
let selectedVoice = 'Lily';
|
| 29 |
let selectedModel = 'Chatterbox';
|
|
|
|
| 51 |
let errorMessage = '';
|
| 52 |
let errorDetails = '';
|
| 53 |
let currentUsername = null;
|
| 54 |
+
|
| 55 |
const models = [
|
| 56 |
{ id: 'chatterbox', name: 'Chatterbox', badge: 'recommended' },
|
| 57 |
{ id: 'kokoro', name: 'Kokoro', badge: 'coming soon', disabled: true },
|
| 58 |
];
|
| 59 |
+
|
| 60 |
const voices = [
|
| 61 |
+
{
|
| 62 |
+
id: 'lily',
|
| 63 |
+
name: 'Lily',
|
| 64 |
+
description: 'Warm, conversational voice from a female in her 30s',
|
| 65 |
+
sample: '/voices/lily.mp3',
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
id: 'andrew',
|
| 69 |
+
name: 'Andrew',
|
| 70 |
+
description: 'Older British man who speaks clearly and kindly',
|
| 71 |
+
sample: '/voices/andrew.mp3',
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
id: 'fairy',
|
| 75 |
+
name: 'Fairy',
|
| 76 |
+
description: 'High and airy female voice that bursts with excitement',
|
| 77 |
+
sample: '/voices/fairy.mp3',
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
id: 'pirate',
|
| 81 |
+
name: 'Pirate',
|
| 82 |
+
description: 'Young pirate that speaks gruffly and passionately',
|
| 83 |
+
sample: '/voices/pirate.mp3',
|
| 84 |
+
},
|
| 85 |
];
|
| 86 |
+
|
| 87 |
async function generateSpeech() {
|
| 88 |
if (!text.trim()) return;
|
| 89 |
+
|
| 90 |
const accessToken = getAccessToken();
|
| 91 |
if (!accessToken) {
|
| 92 |
window.dispatchEvent(new CustomEvent('show-signin-popover'));
|
| 93 |
return;
|
| 94 |
}
|
| 95 |
+
|
| 96 |
isGenerating = true;
|
| 97 |
audioUrl = null;
|
| 98 |
currentTime = 0;
|
| 99 |
+
|
| 100 |
// Generate setup codes if not already present
|
| 101 |
if (!setupCode) {
|
| 102 |
setupCode = generateSetupCode();
|
|
|
|
| 104 |
if (!importCode) {
|
| 105 |
importCode = generateImportCode();
|
| 106 |
}
|
| 107 |
+
|
| 108 |
const ttsCode = generateTTSCode();
|
| 109 |
+
|
| 110 |
if (viewMode === 'ui') {
|
| 111 |
codeButtonFlash = true;
|
| 112 |
setTimeout(() => {
|
|
|
|
| 114 |
}, 2500);
|
| 115 |
}
|
| 116 |
isPlaying = false;
|
| 117 |
+
|
| 118 |
audioTitle = text.length > 30 ? text.substring(0, 30) + '...' : text;
|
| 119 |
+
|
| 120 |
try {
|
| 121 |
const accessToken = getAccessToken();
|
| 122 |
+
|
| 123 |
const requestBody = {
|
| 124 |
text: text,
|
| 125 |
voice_id: selectedVoice.toLowerCase(),
|
|
|
|
| 128 |
access_token: accessToken,
|
| 129 |
parameters: {
|
| 130 |
exaggeration: exaggeration,
|
| 131 |
+
temperature: temperature,
|
| 132 |
+
},
|
| 133 |
};
|
| 134 |
+
|
| 135 |
const response = await fetch('/api/tts/generate', {
|
| 136 |
method: 'POST',
|
| 137 |
headers: {
|
| 138 |
'Content-Type': 'application/json',
|
| 139 |
},
|
| 140 |
+
body: JSON.stringify(requestBody),
|
| 141 |
});
|
| 142 |
+
|
| 143 |
if (!response.ok) {
|
| 144 |
const errorText = await response.text();
|
| 145 |
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
|
| 146 |
}
|
| 147 |
+
|
| 148 |
const result = await response.json();
|
| 149 |
+
|
| 150 |
if (result.success && result.audio_url) {
|
| 151 |
audioUrl = result.audio_url;
|
| 152 |
+
|
| 153 |
// Add to history with result
|
| 154 |
addCodeToHistory(ttsCode, {
|
| 155 |
type: 'audio',
|
| 156 |
url: result.audio_url,
|
| 157 |
title: audioTitle,
|
| 158 |
+
duration: result.duration,
|
| 159 |
});
|
| 160 |
+
|
| 161 |
if (viewMode === 'ui') {
|
| 162 |
codeButtonFlash = true;
|
| 163 |
setTimeout(() => {
|
|
|
|
| 169 |
showError('Generation Failed', errorMessage);
|
| 170 |
audioUrl = null;
|
| 171 |
}
|
|
|
|
| 172 |
} catch (error) {
|
| 173 |
+
showError(
|
| 174 |
+
'Network Error',
|
| 175 |
+
'Failed to connect to the server. Please check your connection and try again.'
|
| 176 |
+
);
|
| 177 |
audioUrl = null;
|
| 178 |
} finally {
|
| 179 |
isGenerating = false;
|
| 180 |
}
|
| 181 |
}
|
| 182 |
+
|
| 183 |
function getAccessToken() {
|
| 184 |
if (typeof window !== 'undefined' && window.gradio && window.gradio.auth_token) {
|
| 185 |
return window.gradio.auth_token;
|
| 186 |
}
|
| 187 |
+
|
| 188 |
const metaToken = document.querySelector('meta[name="hf-oauth-token"]');
|
| 189 |
if (metaToken) {
|
| 190 |
const token = metaToken.getAttribute('content');
|
|
|
|
| 192 |
return token;
|
| 193 |
}
|
| 194 |
}
|
| 195 |
+
|
| 196 |
const possibleKeys = [
|
| 197 |
'hf_access_token',
|
| 198 |
+
'hf_token',
|
| 199 |
'huggingface_token',
|
| 200 |
'oauth_token',
|
| 201 |
+
'access_token',
|
| 202 |
];
|
| 203 |
+
|
| 204 |
for (const key of possibleKeys) {
|
| 205 |
const token = localStorage.getItem(key);
|
| 206 |
if (token) {
|
| 207 |
return token;
|
| 208 |
}
|
| 209 |
}
|
| 210 |
+
|
| 211 |
for (const key of possibleKeys) {
|
| 212 |
const token = sessionStorage.getItem(key);
|
| 213 |
if (token) {
|
| 214 |
return token;
|
| 215 |
}
|
| 216 |
}
|
| 217 |
+
|
| 218 |
const cookies = document.cookie.split(';');
|
| 219 |
for (const cookie of cookies) {
|
| 220 |
const [name, value] = cookie.trim().split('=');
|
|
|
|
| 222 |
return decodeURIComponent(value);
|
| 223 |
}
|
| 224 |
}
|
| 225 |
+
|
| 226 |
try {
|
| 227 |
const authHeader = document.querySelector('script[data-hf-token]');
|
| 228 |
if (authHeader) {
|
|
|
|
| 232 |
}
|
| 233 |
}
|
| 234 |
} catch (e) {}
|
| 235 |
+
|
| 236 |
return null;
|
| 237 |
}
|
| 238 |
+
|
| 239 |
function togglePlayPause() {
|
| 240 |
if (audioElement) {
|
| 241 |
if (isPlaying) {
|
|
|
|
| 245 |
}
|
| 246 |
}
|
| 247 |
}
|
| 248 |
+
|
| 249 |
function handleAudioLoad() {
|
| 250 |
if (audioElement) {
|
| 251 |
duration = audioElement.duration;
|
| 252 |
}
|
| 253 |
}
|
| 254 |
+
|
| 255 |
function handleTimeUpdate() {
|
| 256 |
if (audioElement) {
|
| 257 |
currentTime = audioElement.currentTime;
|
| 258 |
}
|
| 259 |
}
|
| 260 |
+
|
| 261 |
function handlePlay() {
|
| 262 |
isPlaying = true;
|
| 263 |
}
|
| 264 |
+
|
| 265 |
function handlePause() {
|
| 266 |
isPlaying = false;
|
| 267 |
}
|
| 268 |
+
|
| 269 |
function formatTime(seconds) {
|
| 270 |
const mins = Math.floor(seconds / 60);
|
| 271 |
const secs = Math.floor(seconds % 60);
|
| 272 |
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
| 273 |
}
|
| 274 |
+
|
| 275 |
function downloadAudio() {
|
| 276 |
if (audioUrl) {
|
| 277 |
const a = document.createElement('a');
|
|
|
|
| 280 |
a.click();
|
| 281 |
}
|
| 282 |
}
|
| 283 |
+
|
| 284 |
+
function shareAudio() {}
|
| 285 |
+
|
|
|
|
| 286 |
function playSampleVoice(voice, event) {
|
| 287 |
event.stopPropagation();
|
| 288 |
+
|
| 289 |
if (playingSampleVoice === voice.name) {
|
| 290 |
if (sampleAudioElement) {
|
| 291 |
sampleAudioElement.pause();
|
|
|
|
| 297 |
sampleAudioElement.pause();
|
| 298 |
}
|
| 299 |
playingSampleVoice = voice.name;
|
| 300 |
+
|
| 301 |
const sampleUrl = voice.sample || '/samples/harvard.wav';
|
| 302 |
+
|
| 303 |
if (!sampleAudioElement) {
|
| 304 |
sampleAudioElement = new Audio(sampleUrl);
|
| 305 |
sampleAudioElement.addEventListener('ended', () => {
|
|
|
|
| 308 |
} else {
|
| 309 |
sampleAudioElement.src = sampleUrl;
|
| 310 |
}
|
| 311 |
+
|
| 312 |
+
sampleAudioElement.play().catch((err) => {
|
| 313 |
playingSampleVoice = null;
|
| 314 |
});
|
| 315 |
}
|
| 316 |
}
|
| 317 |
+
|
| 318 |
function handleKeyDown(event) {
|
| 319 |
if (event.key === 'Enter' && !event.shiftKey) {
|
| 320 |
event.preventDefault();
|
| 321 |
generateSpeech();
|
| 322 |
}
|
| 323 |
}
|
| 324 |
+
|
| 325 |
function handleClickOutside(event) {
|
| 326 |
if (!event.target.closest('.model-dropdown')) {
|
| 327 |
modelDropdownOpen = false;
|
|
|
|
| 332 |
const entry = {
|
| 333 |
id: Date.now() + Math.random(),
|
| 334 |
code,
|
| 335 |
+
result,
|
| 336 |
};
|
| 337 |
codeHistory = [...codeHistory, entry];
|
| 338 |
saveHistoryToStorage();
|
| 339 |
return entry;
|
| 340 |
}
|
| 341 |
+
|
| 342 |
function saveHistoryToStorage() {
|
| 343 |
if (!currentUsername) return;
|
| 344 |
const storageKey = `hfstudio_history_${currentUsername}`;
|
|
|
|
| 346 |
username: currentUsername,
|
| 347 |
setupCode,
|
| 348 |
importCode,
|
| 349 |
+
history: codeHistory,
|
| 350 |
};
|
| 351 |
localStorage.setItem(storageKey, JSON.stringify(historyData));
|
| 352 |
}
|
| 353 |
+
|
| 354 |
function loadHistoryFromStorage() {
|
| 355 |
if (!currentUsername) return;
|
| 356 |
const storageKey = `hfstudio_history_${currentUsername}`;
|
|
|
|
| 368 |
}
|
| 369 |
}
|
| 370 |
}
|
| 371 |
+
|
| 372 |
function resetHistory() {
|
| 373 |
codeHistory = [];
|
| 374 |
setupCode = null;
|
|
|
|
| 388 |
pip install huggingface-hub`;
|
| 389 |
}
|
| 390 |
}
|
| 391 |
+
|
| 392 |
function generateImportCode() {
|
| 393 |
if (mode === 'local') {
|
| 394 |
return `from huggingface_hub import InferenceClient
|
|
|
|
| 411 |
|
| 412 |
function generateTTSCode() {
|
| 413 |
const voiceUrls = {
|
| 414 |
+
lily: 'https://huggingface.co/spaces/Abradi/hfstudio/resolve/main/voices/lily.mp3',
|
| 415 |
+
andrew: 'https://huggingface.co/spaces/Abradi/hfstudio/resolve/main/voices/andrew.mp3',
|
| 416 |
+
fairy: 'https://huggingface.co/spaces/Abradi/hfstudio/resolve/main/voices/fairy.mp3',
|
| 417 |
+
pirate: 'https://huggingface.co/spaces/Abradi/hfstudio/resolve/main/voices/pirate.mp3',
|
| 418 |
};
|
| 419 |
+
|
| 420 |
if (mode === 'local') {
|
| 421 |
return `# Generate speech
|
| 422 |
text = """${text}"""
|
|
|
|
| 504 |
fetchUserInfo(token);
|
| 505 |
}
|
| 506 |
};
|
| 507 |
+
|
| 508 |
checkUsername();
|
| 509 |
+
|
| 510 |
// Listen for auth changes
|
| 511 |
window.addEventListener('storage', (e) => {
|
| 512 |
if (e.key === 'hf_access_token') {
|
|
|
|
| 514 |
}
|
| 515 |
});
|
| 516 |
});
|
| 517 |
+
|
| 518 |
async function fetchUserInfo(token) {
|
| 519 |
try {
|
| 520 |
const response = await fetch('https://huggingface.co/api/whoami-v2', {
|
| 521 |
headers: {
|
| 522 |
+
Authorization: `Bearer ${token}`,
|
| 523 |
+
},
|
| 524 |
});
|
| 525 |
+
|
| 526 |
if (response.ok) {
|
| 527 |
const userData = await response.json();
|
| 528 |
+
currentUsername =
|
| 529 |
+
userData.name || userData.fullname || userData.login || userData.username || 'User';
|
| 530 |
loadHistoryFromStorage();
|
| 531 |
}
|
| 532 |
} catch (error) {
|
| 533 |
console.error('Error fetching user info:', error);
|
| 534 |
}
|
| 535 |
}
|
| 536 |
+
|
| 537 |
// Update setup/import codes when mode changes
|
| 538 |
$: if (mode) {
|
| 539 |
setupCode = generateSetupCode();
|
| 540 |
importCode = generateImportCode();
|
| 541 |
}
|
| 542 |
+
|
|
|
|
| 543 |
function toggleHistoryAudio(entry) {
|
| 544 |
if (!entry.audioElement) {
|
| 545 |
// Create audio element if it doesn't exist
|
|
|
|
| 549 |
codeHistory = [...codeHistory]; // Trigger reactivity
|
| 550 |
});
|
| 551 |
}
|
| 552 |
+
|
| 553 |
if (entry.isPlaying) {
|
| 554 |
entry.audioElement.pause();
|
| 555 |
entry.isPlaying = false;
|
| 556 |
} else {
|
| 557 |
// Pause any other playing audio
|
| 558 |
+
codeHistory.forEach((e) => {
|
| 559 |
if (e !== entry && e.isPlaying && e.audioElement) {
|
| 560 |
e.audioElement.pause();
|
| 561 |
e.isPlaying = false;
|
|
|
|
| 566 |
}
|
| 567 |
codeHistory = [...codeHistory]; // Trigger reactivity
|
| 568 |
}
|
| 569 |
+
|
| 570 |
function downloadHistoryAudio(url, title) {
|
| 571 |
const link = document.createElement('a');
|
| 572 |
link.href = url;
|
|
|
|
| 575 |
link.click();
|
| 576 |
document.body.removeChild(link);
|
| 577 |
}
|
| 578 |
+
|
| 579 |
function formatDuration(seconds) {
|
| 580 |
if (!seconds) return '0:00';
|
| 581 |
const mins = Math.floor(seconds / 60);
|
| 582 |
const secs = Math.floor(seconds % 60);
|
| 583 |
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
| 584 |
}
|
|
|
|
| 585 |
</script>
|
| 586 |
|
| 587 |
<div class="flex flex-col h-full" on:click={handleClickOutside}>
|
|
|
|
| 592 |
<!-- View mode toggle -->
|
| 593 |
<div class="flex items-center bg-gray-100 rounded-md p-0.5">
|
| 594 |
<button
|
| 595 |
+
class="flex items-center gap-1.5 px-3 py-1 text-sm font-medium rounded transition-colors {viewMode ===
|
| 596 |
+
'ui'
|
| 597 |
+
? 'bg-white shadow-sm'
|
| 598 |
+
: 'text-gray-600'}"
|
| 599 |
+
on:click={() => (viewMode = 'ui')}
|
| 600 |
>
|
| 601 |
<Layout size={14} />
|
| 602 |
UI
|
| 603 |
</button>
|
| 604 |
<button
|
| 605 |
+
class="flex items-center gap-1.5 px-3 py-1 text-sm font-medium rounded transition-colors relative overflow-hidden {viewMode ===
|
| 606 |
+
'code'
|
| 607 |
+
? 'bg-white shadow-sm'
|
| 608 |
+
: 'text-gray-600'} {codeButtonFlash ? 'code-flash' : ''}"
|
| 609 |
+
on:click={() => (viewMode = 'code')}
|
| 610 |
>
|
| 611 |
<Code size={14} />
|
| 612 |
Code Recorder
|
| 613 |
{#if codeButtonFlash}
|
| 614 |
+
<span class="flash-sweep"></span>
|
| 615 |
{/if}
|
| 616 |
</button>
|
| 617 |
</div>
|
| 618 |
</div>
|
| 619 |
</div>
|
| 620 |
</header>
|
| 621 |
+
|
| 622 |
<!-- Main content area -->
|
| 623 |
{#if viewMode === 'ui'}
|
| 624 |
+
<div class="flex-1 flex">
|
| 625 |
+
<!-- Main content area -->
|
| 626 |
+
<div class="flex-1 flex flex-col p-6">
|
| 627 |
+
<!-- Text input area -->
|
| 628 |
+
<div class="flex-1 pb-24">
|
| 629 |
+
<textarea
|
| 630 |
+
bind:value={text}
|
| 631 |
+
class="w-full h-full p-6 bg-white resize-none border-0 focus:outline-none text-gray-900 text-base leading-relaxed"
|
| 632 |
+
placeholder="In a hole in the ground there lived a hobbit."
|
| 633 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
</div>
|
| 635 |
+
|
| 636 |
+
<!-- Fixed bottom generate button -->
|
| 637 |
+
<div class="fixed bottom-0 left-56 right-80 p-4 bg-white border-t border-gray-200">
|
| 638 |
+
<div class="flex items-center justify-between mb-3">
|
| 639 |
+
<span class="text-sm text-gray-500">{text.length} / 5,000 characters</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 640 |
</div>
|
| 641 |
+
<button
|
| 642 |
+
on:click={generateSpeech}
|
| 643 |
+
disabled={isGenerating || !text.trim()}
|
| 644 |
+
class="w-full px-6 py-3 bg-gradient-to-r from-amber-400 to-orange-500 text-white rounded-lg font-medium hover:from-amber-500 hover:to-orange-600 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 shadow-sm"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
>
|
| 646 |
+
{#if isGenerating}
|
| 647 |
+
<Loader2 size={20} class="animate-spin" />
|
| 648 |
+
Generating...
|
| 649 |
{:else}
|
| 650 |
+
<Play size={20} />
|
| 651 |
+
Generate speech
|
| 652 |
{/if}
|
| 653 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
</div>
|
| 655 |
+
|
| 656 |
+
<!-- Generated audio section -->
|
| 657 |
{#if audioUrl}
|
| 658 |
+
<div class="p-4 border border-gray-200 rounded-lg bg-white">
|
| 659 |
+
<!-- Audio title and voice info -->
|
| 660 |
+
<div class="flex items-center gap-3 mb-4">
|
| 661 |
+
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
| 662 |
+
<div class="flex-1">
|
| 663 |
+
<h3 class="font-medium text-gray-900 text-sm">{audioTitle}</h3>
|
| 664 |
+
<p class="text-xs text-gray-500">{selectedVoice} β’ Created 1 second ago</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
</div>
|
| 666 |
+
<!-- Mini action buttons -->
|
| 667 |
+
<div class="flex items-center gap-2">
|
| 668 |
+
<button
|
| 669 |
+
on:click={shareAudio}
|
| 670 |
+
class="flex items-center gap-1.5 px-3 py-1.5 text-sm border border-gray-200 rounded-md hover:bg-gray-50 transition-colors"
|
| 671 |
+
>
|
| 672 |
+
<Share size={14} class="text-gray-600" />
|
| 673 |
+
<span class="text-gray-700">Share</span>
|
| 674 |
+
</button>
|
| 675 |
+
<button
|
| 676 |
+
on:click={downloadAudio}
|
| 677 |
+
class="flex items-center gap-1.5 px-3 py-1.5 text-sm border border-gray-200 rounded-md hover:bg-gray-50 transition-colors"
|
| 678 |
+
>
|
| 679 |
+
<span class="text-gray-700">Download</span>
|
| 680 |
+
<Download size={14} class="text-gray-600" />
|
| 681 |
+
</button>
|
| 682 |
</div>
|
| 683 |
</div>
|
| 684 |
+
|
| 685 |
+
<!-- Mini audio controls -->
|
| 686 |
+
<div class="flex items-center gap-3 mb-4">
|
| 687 |
+
<!-- Play/Pause button -->
|
| 688 |
+
<button
|
| 689 |
+
on:click={togglePlayPause}
|
| 690 |
+
class="w-8 h-8 bg-black rounded-full flex items-center justify-center hover:bg-gray-800 transition-colors"
|
| 691 |
+
>
|
| 692 |
+
{#if isPlaying}
|
| 693 |
+
<div class="pause-filled text-white"></div>
|
| 694 |
+
{:else}
|
| 695 |
+
<Play size={14} class="text-white ml-0.5" />
|
| 696 |
+
{/if}
|
| 697 |
+
</button>
|
| 698 |
+
|
| 699 |
+
<!-- Progress bar -->
|
| 700 |
+
<div class="flex-1 flex items-center gap-2">
|
| 701 |
+
<span class="text-xs text-gray-500 font-mono">{formatTime(currentTime)}</span>
|
| 702 |
+
<div class="flex-1 h-1 bg-gray-200 rounded-full cursor-pointer">
|
| 703 |
+
<div
|
| 704 |
+
class="h-full bg-gradient-to-r from-amber-400 to-orange-500 rounded-full transition-all"
|
| 705 |
+
style="width: {(currentTime / duration) * 100}%"
|
| 706 |
+
></div>
|
| 707 |
+
</div>
|
| 708 |
+
<span class="text-xs text-gray-500 font-mono">{formatTime(duration)}</span>
|
| 709 |
</div>
|
| 710 |
+
</div>
|
| 711 |
+
|
| 712 |
+
<!-- Full audio player controls -->
|
| 713 |
+
<div class="flex items-center gap-4 mb-4">
|
| 714 |
+
<!-- Skip back button -->
|
| 715 |
+
<button class="p-2 hover:bg-gray-100 rounded-full" title="Skip back">
|
| 716 |
+
<SkipBack size={20} class="text-gray-600" />
|
| 717 |
+
</button>
|
| 718 |
+
|
| 719 |
+
<!-- Play/Pause button -->
|
| 720 |
+
<button
|
| 721 |
+
on:click={togglePlayPause}
|
| 722 |
+
class="w-12 h-12 bg-black rounded-full flex items-center justify-center hover:bg-gray-800 transition-colors"
|
| 723 |
+
>
|
| 724 |
+
{#if isPlaying}
|
| 725 |
+
<div class="pause-filled text-white scale-150"></div>
|
| 726 |
+
{:else}
|
| 727 |
+
<Play size={20} class="text-white ml-0.5" />
|
| 728 |
+
{/if}
|
| 729 |
+
</button>
|
| 730 |
+
|
| 731 |
+
<!-- Skip forward button -->
|
| 732 |
+
<button class="p-2 hover:bg-gray-100 rounded-full" title="Skip forward">
|
| 733 |
+
<SkipForward size={20} class="text-gray-600" />
|
| 734 |
+
</button>
|
| 735 |
+
|
| 736 |
+
<!-- Progress bar -->
|
| 737 |
+
<div class="flex-1 flex items-center gap-3">
|
| 738 |
+
<span class="text-xs text-gray-500 font-mono">{formatTime(currentTime)}</span>
|
| 739 |
+
<div class="flex-1 h-1 bg-gray-200 rounded-full">
|
| 740 |
+
<div
|
| 741 |
+
class="h-full bg-gradient-to-r from-amber-400 to-orange-500 rounded-full transition-all"
|
| 742 |
+
style="width: {(currentTime / duration) * 100}%"
|
| 743 |
+
></div>
|
| 744 |
</div>
|
| 745 |
+
<span class="text-xs text-gray-500 font-mono">{formatTime(duration)}</span>
|
| 746 |
+
</div>
|
| 747 |
+
|
| 748 |
+
<!-- Action buttons -->
|
| 749 |
+
<div class="flex items-center gap-2">
|
| 750 |
+
<button
|
| 751 |
+
on:click={shareAudio}
|
| 752 |
+
class="flex items-center gap-2 px-3 py-1.5 text-sm border border-gray-200 rounded-md hover:bg-gray-50"
|
| 753 |
+
>
|
| 754 |
+
<Share size={14} />
|
| 755 |
+
Share
|
| 756 |
+
</button>
|
| 757 |
+
<button
|
| 758 |
+
on:click={downloadAudio}
|
| 759 |
+
class="p-2 hover:bg-gray-100 rounded-md"
|
| 760 |
+
title="Download"
|
| 761 |
+
>
|
| 762 |
+
<Download size={16} class="text-gray-600" />
|
| 763 |
+
</button>
|
| 764 |
+
<button class="p-2 hover:bg-gray-100 rounded-md" title="More options">
|
| 765 |
+
<MoreHorizontal size={16} class="text-gray-600" />
|
| 766 |
+
</button>
|
| 767 |
</div>
|
| 768 |
</div>
|
| 769 |
+
|
| 770 |
+
<!-- Hidden audio element -->
|
| 771 |
+
{#if audioUrl}
|
| 772 |
+
<audio
|
| 773 |
+
bind:this={audioElement}
|
| 774 |
+
src={audioUrl}
|
| 775 |
+
on:loadedmetadata={handleAudioLoad}
|
| 776 |
+
on:timeupdate={handleTimeUpdate}
|
| 777 |
+
on:play={handlePlay}
|
| 778 |
+
on:pause={handlePause}
|
| 779 |
+
style="display: none;"
|
| 780 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
</div>
|
| 783 |
+
{/if}
|
| 784 |
+
</div>
|
| 785 |
+
|
| 786 |
+
<!-- Right panel -->
|
| 787 |
+
<div class="w-80 border-l border-gray-200 bg-white p-4 overflow-y-auto">
|
| 788 |
+
<!-- Model selector -->
|
| 789 |
+
<div class="mb-6 relative model-dropdown">
|
| 790 |
+
<h3 class="font-medium text-gray-900 mb-3">Model</h3>
|
| 791 |
+
<button
|
| 792 |
+
on:click={() => (modelDropdownOpen = !modelDropdownOpen)}
|
| 793 |
+
class="w-full p-3 border border-gray-200 rounded-lg bg-white text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 focus:border-transparent appearance-none bg-no-repeat bg-right pr-10 shadow-sm text-left flex items-center justify-between"
|
| 794 |
+
>
|
| 795 |
+
<span>
|
| 796 |
+
{#each models as model}
|
| 797 |
+
{#if model.name === selectedModel}
|
| 798 |
+
{model.name}{#if model.badge} <span class="text-xs text-gray-500"
|
| 799 |
+
>({model.badge})</span
|
| 800 |
+
>{/if}
|
| 801 |
+
{/if}
|
| 802 |
+
{/each}
|
| 803 |
+
</span>
|
| 804 |
+
<ChevronDown size={16} class="text-gray-500" />
|
| 805 |
+
</button>
|
| 806 |
+
|
| 807 |
+
{#if modelDropdownOpen}
|
| 808 |
+
<div
|
| 809 |
+
class="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-10"
|
| 810 |
>
|
| 811 |
+
{#each models as model}
|
| 812 |
+
<button
|
| 813 |
+
class="w-full px-3 py-2 text-left transition-colors text-sm {model.disabled
|
| 814 |
+
? 'opacity-50 cursor-not-allowed'
|
| 815 |
+
: 'hover:bg-gray-50'} {model.name === selectedModel ? 'bg-gray-100' : ''}"
|
| 816 |
+
disabled={model.disabled}
|
| 817 |
+
on:click={() => {
|
| 818 |
+
if (!model.disabled) {
|
| 819 |
+
selectedModel = model.name;
|
| 820 |
+
modelDropdownOpen = false;
|
| 821 |
+
}
|
| 822 |
+
}}
|
| 823 |
+
>
|
| 824 |
+
{model.name}{#if model.badge} <span class="text-xs text-gray-500"
|
| 825 |
+
>({model.badge})</span
|
| 826 |
+
>{/if}
|
| 827 |
+
</button>
|
| 828 |
+
{/each}
|
| 829 |
+
</div>
|
| 830 |
+
{/if}
|
| 831 |
+
|
| 832 |
+
<!-- Pricing info -->
|
| 833 |
+
<div class="mt-2 text-xs text-gray-500">
|
| 834 |
+
Estimated $0.025 per 1000 characters β’ <a
|
| 835 |
+
href="https://huggingface.co/settings/billing"
|
| 836 |
+
target="_blank"
|
| 837 |
+
class="text-amber-600 hover:text-amber-700 underline">Billing ‴</a
|
| 838 |
>
|
|
|
|
|
|
|
|
|
|
| 839 |
</div>
|
|
|
|
| 840 |
</div>
|
|
|
|
| 841 |
|
| 842 |
+
<div class="mb-6">
|
| 843 |
+
<div class="mb-3">
|
| 844 |
+
<h3 class="font-medium text-gray-900">Voice</h3>
|
| 845 |
+
</div>
|
| 846 |
+
|
| 847 |
+
<div class="space-y-2">
|
| 848 |
+
{#each voices as voice}
|
| 849 |
+
<button
|
| 850 |
+
class="w-full flex items-center justify-between p-2 rounded-lg hover:bg-gray-50 transition-colors text-left group border border-transparent
|
| 851 |
+
{voice.name === selectedVoice ? 'bg-gray-100 border-gray-200' : ''}"
|
| 852 |
+
on:click={() => (selectedVoice = voice.name)}
|
| 853 |
+
>
|
| 854 |
+
<div class="flex items-center gap-3 flex-1 min-w-0">
|
| 855 |
+
<div
|
| 856 |
+
class="w-10 h-10 bg-gradient-to-br from-amber-400 to-orange-500 rounded-full flex items-center justify-center text-white text-sm font-semibold flex-shrink-0"
|
| 857 |
+
>
|
| 858 |
+
{voice.name[0]}
|
| 859 |
+
</div>
|
| 860 |
+
<div class="flex-1 min-w-0">
|
| 861 |
+
<div class="text-sm font-medium text-gray-900 mb-1">{voice.name}</div>
|
| 862 |
+
<div class="text-xs text-gray-500 leading-relaxed">
|
| 863 |
+
{voice.description}
|
| 864 |
+
</div>
|
| 865 |
+
</div>
|
| 866 |
+
</div>
|
| 867 |
+
<button
|
| 868 |
+
on:click={(e) => playSampleVoice(voice, e)}
|
| 869 |
+
class="p-2 rounded-full hover:bg-gray-200 transition-colors flex-shrink-0 ml-2"
|
| 870 |
+
title="Play sample"
|
| 871 |
+
>
|
| 872 |
+
{#if playingSampleVoice === voice.name}
|
| 873 |
+
<Pause size={16} class="text-gray-600" />
|
| 874 |
+
{:else}
|
| 875 |
+
<Play size={16} class="text-gray-600" />
|
| 876 |
+
{/if}
|
| 877 |
+
</button>
|
| 878 |
+
</button>
|
| 879 |
+
{/each}
|
| 880 |
+
|
| 881 |
+
<!-- Clone voice option -->
|
| 882 |
<button
|
| 883 |
+
class="w-full flex items-center justify-between p-2 rounded-lg opacity-50 cursor-not-allowed text-left border border-transparent"
|
| 884 |
+
disabled
|
|
|
|
| 885 |
>
|
| 886 |
+
<div class="flex items-center gap-3 flex-1 min-w-0">
|
| 887 |
+
<div
|
| 888 |
+
class="w-10 h-10 bg-gray-400 rounded-full flex items-center justify-center text-white text-sm font-medium flex-shrink-0"
|
| 889 |
+
>
|
| 890 |
+
+
|
| 891 |
+
</div>
|
| 892 |
+
<div class="flex-1 min-w-0">
|
| 893 |
+
<div class="text-sm font-medium text-gray-600 mb-1">Clone your voice</div>
|
| 894 |
+
<div class="text-xs text-gray-400">(coming soon)</div>
|
| 895 |
+
</div>
|
| 896 |
+
</div>
|
| 897 |
</button>
|
| 898 |
</div>
|
|
|
|
|
|
|
|
|
|
| 899 |
</div>
|
| 900 |
+
|
| 901 |
+
<div class="space-y-4 pt-4 border-t border-gray-200">
|
| 902 |
+
<!-- Exaggeration control -->
|
| 903 |
+
<div>
|
| 904 |
+
<div class="flex justify-between mb-1">
|
| 905 |
+
<label for="exaggeration-slider" class="text-sm font-medium text-gray-700"
|
| 906 |
+
>Exaggeration</label
|
| 907 |
+
>
|
| 908 |
+
<span class="text-sm text-gray-500">{exaggeration.toFixed(2)}</span>
|
| 909 |
+
</div>
|
| 910 |
+
<input
|
| 911 |
+
id="exaggeration-slider"
|
| 912 |
+
type="range"
|
| 913 |
+
bind:value={exaggeration}
|
| 914 |
+
min="0"
|
| 915 |
+
max="1"
|
| 916 |
+
step="0.01"
|
| 917 |
+
class="w-full h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-hf"
|
| 918 |
+
/>
|
| 919 |
+
<div class="flex justify-between text-xs text-gray-400 mt-1">
|
| 920 |
+
<span>None</span>
|
| 921 |
+
<span>Exaggerated</span>
|
| 922 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 923 |
</div>
|
| 924 |
+
|
| 925 |
+
<!-- Stability control -->
|
| 926 |
+
<div>
|
| 927 |
+
<div class="flex justify-between mb-1">
|
| 928 |
+
<label for="temperature-slider" class="text-sm font-medium text-gray-700"
|
| 929 |
+
>Stability</label
|
| 930 |
+
>
|
| 931 |
+
<span class="text-sm text-gray-500">{temperature.toFixed(2)}</span>
|
| 932 |
+
</div>
|
| 933 |
+
<input
|
| 934 |
+
id="temperature-slider"
|
| 935 |
+
type="range"
|
| 936 |
+
bind:value={temperature}
|
| 937 |
+
min="0"
|
| 938 |
+
max="1"
|
| 939 |
+
step="0.01"
|
| 940 |
+
class="w-full h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-hf"
|
| 941 |
+
/>
|
| 942 |
+
<div class="flex justify-between text-xs text-gray-400 mt-1">
|
| 943 |
+
<span>More stable</span>
|
| 944 |
+
<span>More variable</span>
|
| 945 |
+
</div>
|
| 946 |
</div>
|
| 947 |
</div>
|
| 948 |
+
</div>
|
| 949 |
+
</div>
|
| 950 |
+
{:else}
|
| 951 |
+
<!-- Code view -->
|
| 952 |
+
<div class="flex-1 bg-gray-50 overflow-y-auto">
|
| 953 |
+
<div class="max-w-4xl mx-auto p-8">
|
| 954 |
+
<!-- Header -->
|
| 955 |
+
<div class="mb-6">
|
| 956 |
+
<div>
|
| 957 |
+
<h2 class="text-2xl font-semibold text-gray-900">Integration Code</h2>
|
| 958 |
+
<p class="text-sm text-gray-600 mt-1">
|
| 959 |
+
{#if mode === 'local'}
|
| 960 |
+
Python code to reproduce your actions using a local HFStudio server
|
| 961 |
+
{:else}
|
| 962 |
+
Python code to reproduce your actions via the API
|
| 963 |
+
{/if}
|
| 964 |
+
</p>
|
| 965 |
+
</div>
|
| 966 |
+
|
| 967 |
+
<!-- Toggle and Copy All button row -->
|
| 968 |
+
<div class="flex items-center justify-between mt-4">
|
| 969 |
+
<!-- API/Local Mode Toggle -->
|
| 970 |
+
<div class="flex items-center bg-gray-100 rounded-md p-0.5">
|
| 971 |
<button
|
| 972 |
+
class="px-3 py-1 text-sm font-medium rounded transition-colors {mode === 'api'
|
| 973 |
+
? 'bg-white shadow-sm'
|
| 974 |
+
: 'text-gray-600'}"
|
| 975 |
+
on:click={() => (mode = 'api')}
|
| 976 |
>
|
| 977 |
+
API
|
| 978 |
+
</button>
|
| 979 |
+
<button
|
| 980 |
+
class="px-3 py-1 text-sm font-medium rounded transition-colors {mode === 'local'
|
| 981 |
+
? 'bg-white shadow-sm'
|
| 982 |
+
: 'text-gray-600'}"
|
| 983 |
+
on:click={() => (mode = 'local')}
|
| 984 |
+
>
|
| 985 |
+
Local
|
| 986 |
</button>
|
| 987 |
</div>
|
| 988 |
+
|
| 989 |
+
{#if codeHistory.length > 0 || setupCode || importCode}
|
| 990 |
+
<div class="flex items-center gap-2">
|
| 991 |
+
<button
|
| 992 |
+
on:click={resetHistory}
|
| 993 |
+
class="flex items-center bg-red-50 hover:bg-red-100 rounded-md px-3 py-1.5 transition-colors"
|
| 994 |
+
title="Clear history"
|
| 995 |
+
>
|
| 996 |
+
<RotateCcw size={16} class="text-red-600" />
|
| 997 |
+
<span class="ml-2 text-sm font-medium text-red-600">Reset</span>
|
| 998 |
+
</button>
|
| 999 |
+
<button
|
| 1000 |
+
on:click={copyAllCode}
|
| 1001 |
+
class="flex items-center bg-gray-100 hover:bg-gray-200 rounded-md px-3 py-1.5 transition-colors"
|
| 1002 |
+
>
|
| 1003 |
+
<Copy size={16} class="text-gray-600" />
|
| 1004 |
+
<span class="ml-2 text-sm font-medium text-gray-600">Copy All</span>
|
| 1005 |
+
</button>
|
| 1006 |
+
</div>
|
| 1007 |
+
{/if}
|
| 1008 |
+
</div>
|
| 1009 |
+
</div>
|
| 1010 |
+
|
| 1011 |
+
<!-- Code sections -->
|
| 1012 |
+
{#if !setupCode && !importCode && codeHistory.length === 0}
|
| 1013 |
+
<div class="bg-white rounded-lg border border-gray-200 p-8 text-center">
|
| 1014 |
+
<p class="text-gray-500">Start using the UI to see generated code here</p>
|
| 1015 |
+
{#if currentUsername}
|
| 1016 |
+
<p class="text-xs text-gray-400 mt-2">Logged in as: {currentUsername}</p>
|
| 1017 |
+
{/if}
|
| 1018 |
</div>
|
| 1019 |
+
{:else}
|
| 1020 |
+
<div class="space-y-6">
|
| 1021 |
+
<!-- Setup Section -->
|
| 1022 |
+
{#if setupCode}
|
| 1023 |
+
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
| 1024 |
+
<div
|
| 1025 |
+
class="flex items-center justify-between px-4 py-2 bg-amber-50 border-b border-amber-200"
|
| 1026 |
+
>
|
| 1027 |
+
<div class="flex items-center gap-2">
|
| 1028 |
+
<span class="text-sm font-medium text-amber-900">Setup (Terminal)</span>
|
| 1029 |
+
<span class="text-xs bg-amber-100 text-amber-700 px-2 py-0.5 rounded"
|
| 1030 |
+
>Run once</span
|
| 1031 |
+
>
|
| 1032 |
+
</div>
|
| 1033 |
<button
|
| 1034 |
+
on:click={() => copyToClipboard(setupCode)}
|
| 1035 |
+
class="p-1.5 hover:bg-amber-100 rounded transition-colors"
|
| 1036 |
+
title="Copy setup code"
|
| 1037 |
>
|
| 1038 |
+
<Copy size={14} class="text-amber-600" />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1039 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1040 |
</div>
|
| 1041 |
+
<div class="relative">
|
| 1042 |
+
<pre class="p-4 overflow-x-auto bg-gray-50"><code class="language-bash text-sm"
|
| 1043 |
+
>{@html Prism.highlight(setupCode, Prism.languages.bash, 'bash')}</code
|
| 1044 |
+
></pre>
|
| 1045 |
+
</div>
|
| 1046 |
+
</div>
|
| 1047 |
+
{/if}
|
| 1048 |
+
|
| 1049 |
+
<!-- Import Section -->
|
| 1050 |
+
{#if importCode}
|
| 1051 |
+
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
| 1052 |
+
<div
|
| 1053 |
+
class="flex items-center justify-between px-4 py-2 bg-blue-50 border-b border-blue-200"
|
| 1054 |
+
>
|
| 1055 |
+
<div class="flex items-center gap-2">
|
| 1056 |
+
<span class="text-sm font-medium text-blue-900">Imports (Python)</span>
|
| 1057 |
+
<span class="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded"
|
| 1058 |
+
>Run once</span
|
| 1059 |
+
>
|
| 1060 |
+
</div>
|
| 1061 |
<button
|
| 1062 |
+
on:click={() => copyToClipboard(importCode)}
|
| 1063 |
+
class="p-1.5 hover:bg-blue-100 rounded transition-colors"
|
| 1064 |
+
title="Copy import code"
|
| 1065 |
>
|
| 1066 |
+
<Copy size={14} class="text-blue-600" />
|
| 1067 |
</button>
|
| 1068 |
+
</div>
|
| 1069 |
+
<div class="relative">
|
| 1070 |
+
<pre class="p-4 overflow-x-auto bg-gray-50"><code class="language-python text-sm"
|
| 1071 |
+
>{@html Prism.highlight(importCode, Prism.languages.python, 'python')}</code
|
| 1072 |
+
></pre>
|
| 1073 |
+
</div>
|
| 1074 |
+
</div>
|
| 1075 |
+
{/if}
|
| 1076 |
+
|
| 1077 |
+
<!-- History entries -->
|
| 1078 |
+
{#each codeHistory as entry, i (entry.id)}
|
| 1079 |
+
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm">
|
| 1080 |
+
<!-- Code cell -->
|
| 1081 |
+
<div class="border-b border-gray-200">
|
| 1082 |
+
<div
|
| 1083 |
+
class="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-100"
|
| 1084 |
>
|
| 1085 |
+
<span class="text-sm font-medium text-gray-700">Cell {i + 1}</span>
|
| 1086 |
+
<button
|
| 1087 |
+
on:click={() => copyToClipboard(entry.code)}
|
| 1088 |
+
class="p-1.5 hover:bg-gray-200 rounded transition-colors"
|
| 1089 |
+
title="Copy code"
|
| 1090 |
+
>
|
| 1091 |
+
<Copy size={14} class="text-gray-600" />
|
| 1092 |
+
</button>
|
| 1093 |
+
</div>
|
| 1094 |
+
<div class="relative">
|
| 1095 |
+
<pre class="p-4 overflow-x-auto bg-gray-50"><code
|
| 1096 |
+
class="language-python text-sm"
|
| 1097 |
+
>{@html Prism.highlight(entry.code, Prism.languages.python, 'python')}</code
|
| 1098 |
+
></pre>
|
| 1099 |
+
</div>
|
| 1100 |
</div>
|
| 1101 |
+
|
| 1102 |
+
<!-- Result (audio player) -->
|
| 1103 |
+
{#if entry.result && entry.result.type === 'audio'}
|
| 1104 |
+
<div class="bg-gradient-to-b from-gray-50 to-white p-4">
|
| 1105 |
+
<div class="bg-white rounded-lg border border-gray-200 p-4 shadow-sm">
|
| 1106 |
+
<div class="flex items-center justify-between">
|
| 1107 |
+
<div class="flex items-center gap-3 flex-1">
|
| 1108 |
+
<button
|
| 1109 |
+
on:click={() => toggleHistoryAudio(entry)}
|
| 1110 |
+
class="w-10 h-10 bg-gradient-to-r from-amber-500 to-orange-500 rounded-full flex items-center justify-center text-white hover:from-amber-600 hover:to-orange-600 transition-colors shadow-md"
|
| 1111 |
+
>
|
| 1112 |
+
{#if entry.isPlaying}
|
| 1113 |
+
<Pause size={18} />
|
| 1114 |
+
{:else}
|
| 1115 |
+
<Play size={18} class="ml-0.5" />
|
| 1116 |
+
{/if}
|
| 1117 |
+
</button>
|
| 1118 |
+
<div class="flex-1">
|
| 1119 |
+
<div class="text-sm font-medium text-gray-900 truncate">
|
| 1120 |
+
{entry.result.title || 'Generated Audio'}
|
| 1121 |
+
</div>
|
| 1122 |
+
<div class="text-xs text-gray-500">
|
| 1123 |
+
Duration: {formatDuration(entry.result.duration || 0)}
|
| 1124 |
+
</div>
|
| 1125 |
+
</div>
|
| 1126 |
+
</div>
|
| 1127 |
+
<div class="flex items-center gap-1">
|
| 1128 |
+
<button
|
| 1129 |
+
on:click={() =>
|
| 1130 |
+
downloadHistoryAudio(entry.result.url, entry.result.title)}
|
| 1131 |
+
class="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
| 1132 |
+
title="Download"
|
| 1133 |
+
>
|
| 1134 |
+
<Download size={16} class="text-gray-600" />
|
| 1135 |
+
</button>
|
| 1136 |
+
<button
|
| 1137 |
+
class="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
| 1138 |
+
title="Share"
|
| 1139 |
+
>
|
| 1140 |
+
<Share size={16} class="text-gray-600" />
|
| 1141 |
+
</button>
|
| 1142 |
+
</div>
|
| 1143 |
+
</div>
|
| 1144 |
+
<audio
|
| 1145 |
+
bind:this={entry.audioElement}
|
| 1146 |
+
src={entry.result.url}
|
| 1147 |
+
on:ended={() => (entry.isPlaying = false)}
|
| 1148 |
+
class="hidden"
|
| 1149 |
+
/>
|
| 1150 |
+
</div>
|
| 1151 |
+
</div>
|
| 1152 |
+
{/if}
|
| 1153 |
</div>
|
| 1154 |
+
{/each}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1155 |
</div>
|
| 1156 |
+
{/if}
|
|
|
|
|
|
|
| 1157 |
</div>
|
|
|
|
| 1158 |
</div>
|
|
|
|
| 1159 |
{/if}
|
| 1160 |
+
|
| 1161 |
<!-- Copy notification toast -->
|
| 1162 |
{#if copyNotification}
|
| 1163 |
+
<div
|
| 1164 |
+
class="fixed bottom-4 right-4 px-4 py-2 bg-gray-900 text-white rounded-lg shadow-lg z-50 animate-fade-in"
|
| 1165 |
+
>
|
| 1166 |
+
{copyNotification}
|
| 1167 |
+
</div>
|
| 1168 |
{/if}
|
| 1169 |
|
| 1170 |
<!-- Error Modal -->
|
| 1171 |
{#if showErrorModal}
|
| 1172 |
+
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
| 1173 |
+
<div class="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[80vh] flex flex-col">
|
| 1174 |
+
<!-- Header -->
|
| 1175 |
+
<div
|
| 1176 |
+
class="flex items-center justify-between p-6 border-b border-gray-200 bg-red-50 flex-shrink-0"
|
| 1177 |
+
>
|
| 1178 |
+
<div class="flex items-center gap-3 min-w-0">
|
| 1179 |
+
<div
|
| 1180 |
+
class="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center flex-shrink-0"
|
| 1181 |
+
>
|
| 1182 |
+
<AlertCircle size={20} class="text-red-600" />
|
| 1183 |
+
</div>
|
| 1184 |
+
<div class="min-w-0">
|
| 1185 |
+
<h3 class="text-lg font-semibold text-gray-900 truncate">{errorMessage}</h3>
|
| 1186 |
+
<p class="text-sm text-gray-600">An error occurred while processing your request</p>
|
| 1187 |
+
</div>
|
| 1188 |
</div>
|
| 1189 |
+
<button
|
| 1190 |
+
on:click={closeErrorModal}
|
| 1191 |
+
class="p-2 hover:bg-red-100 rounded-full transition-colors flex-shrink-0"
|
| 1192 |
+
title="Close"
|
| 1193 |
+
>
|
| 1194 |
+
<X size={20} class="text-gray-500" />
|
| 1195 |
+
</button>
|
| 1196 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1197 |
|
| 1198 |
+
<!-- Content -->
|
| 1199 |
+
<div class="p-6 overflow-y-auto flex-1 min-h-0">
|
| 1200 |
+
{#if errorDetails}
|
| 1201 |
+
<div class="bg-gray-50 rounded-lg p-4 border">
|
| 1202 |
+
<h4 class="text-sm font-medium text-gray-900 mb-2">Error Details:</h4>
|
| 1203 |
+
<pre
|
| 1204 |
+
class="text-xs text-gray-700 whitespace-pre-wrap font-mono leading-relaxed break-words">{errorDetails}</pre>
|
| 1205 |
+
</div>
|
| 1206 |
+
{/if}
|
| 1207 |
</div>
|
|
|
|
|
|
|
| 1208 |
|
| 1209 |
+
<!-- Footer -->
|
| 1210 |
+
<div
|
| 1211 |
+
class="flex items-center justify-end gap-3 p-6 border-t border-gray-200 bg-gray-50 flex-shrink-0"
|
|
|
|
|
|
|
| 1212 |
>
|
| 1213 |
+
<button
|
| 1214 |
+
on:click={copyErrorMessage}
|
| 1215 |
+
class="flex items-center gap-2 px-4 py-2 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors"
|
| 1216 |
+
>
|
| 1217 |
+
<Copy size={16} />
|
| 1218 |
+
Copy Error
|
| 1219 |
+
</button>
|
| 1220 |
+
<button
|
| 1221 |
+
on:click={closeErrorModal}
|
| 1222 |
+
class="px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
| 1223 |
+
>
|
| 1224 |
+
Close
|
| 1225 |
+
</button>
|
| 1226 |
+
</div>
|
| 1227 |
</div>
|
| 1228 |
</div>
|
|
|
|
| 1229 |
{/if}
|
| 1230 |
</div>
|
| 1231 |
|
| 1232 |
<style>
|
| 1233 |
@keyframes fade-in {
|
| 1234 |
+
from {
|
| 1235 |
+
opacity: 0;
|
| 1236 |
+
transform: translateY(10px);
|
| 1237 |
+
}
|
| 1238 |
+
to {
|
| 1239 |
+
opacity: 1;
|
| 1240 |
+
transform: translateY(0);
|
| 1241 |
+
}
|
| 1242 |
}
|
| 1243 |
+
|
| 1244 |
.animate-fade-in {
|
| 1245 |
animation: fade-in 0.3s ease-out;
|
| 1246 |
}
|
| 1247 |
+
|
| 1248 |
@keyframes sweep {
|
| 1249 |
0% {
|
| 1250 |
left: -100%;
|
|
|
|
| 1259 |
left: 100%;
|
| 1260 |
}
|
| 1261 |
}
|
| 1262 |
+
|
| 1263 |
.flash-sweep {
|
| 1264 |
position: absolute;
|
| 1265 |
top: 0;
|
| 1266 |
left: -100%;
|
| 1267 |
width: 100%;
|
| 1268 |
height: 100%;
|
| 1269 |
+
background: linear-gradient(
|
| 1270 |
+
90deg,
|
| 1271 |
+
transparent 0%,
|
| 1272 |
rgba(251, 191, 36, 0.5) 25%,
|
| 1273 |
rgba(249, 115, 22, 0.8) 50%,
|
| 1274 |
rgba(251, 191, 36, 0.5) 75%,
|
| 1275 |
+
transparent 100%
|
| 1276 |
+
);
|
| 1277 |
animation: sweep 2s ease-in-out;
|
| 1278 |
pointer-events: none;
|
| 1279 |
}
|
| 1280 |
+
|
| 1281 |
.code-flash {
|
| 1282 |
animation: pulse 0.5s ease-out;
|
| 1283 |
}
|
| 1284 |
+
|
| 1285 |
@keyframes pulse {
|
| 1286 |
0% {
|
| 1287 |
box-shadow: 0 0 0 0 rgba(251, 191, 36, 0);
|
|
|
|
| 1293 |
box-shadow: 0 0 0 0 rgba(251, 191, 36, 0);
|
| 1294 |
}
|
| 1295 |
}
|
| 1296 |
+
</style>
|
|
|
hfstudio/__init__.py
CHANGED
|
@@ -4,14 +4,16 @@ import json
|
|
| 4 |
import os
|
| 5 |
from pathlib import Path
|
| 6 |
|
|
|
|
| 7 |
def _get_version():
|
| 8 |
"""Read version from frontend/package.json"""
|
| 9 |
# Get the project root (one level up from this package)
|
| 10 |
package_root = Path(__file__).parent.parent
|
| 11 |
package_json_path = package_root / "frontend" / "package.json"
|
| 12 |
-
|
| 13 |
-
with open(package_json_path,
|
| 14 |
package_data = json.load(f)
|
| 15 |
-
return package_data[
|
|
|
|
| 16 |
|
| 17 |
-
__version__ = _get_version()
|
|
|
|
| 4 |
import os
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
+
|
| 8 |
def _get_version():
|
| 9 |
"""Read version from frontend/package.json"""
|
| 10 |
# Get the project root (one level up from this package)
|
| 11 |
package_root = Path(__file__).parent.parent
|
| 12 |
package_json_path = package_root / "frontend" / "package.json"
|
| 13 |
+
|
| 14 |
+
with open(package_json_path, "r") as f:
|
| 15 |
package_data = json.load(f)
|
| 16 |
+
return package_data["version"]
|
| 17 |
+
|
| 18 |
|
| 19 |
+
__version__ = _get_version()
|
hfstudio/__main__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
from hfstudio.cli import main
|
| 2 |
|
| 3 |
if __name__ == "__main__":
|
| 4 |
-
main()
|
|
|
|
| 1 |
from hfstudio.cli import main
|
| 2 |
|
| 3 |
if __name__ == "__main__":
|
| 4 |
+
main()
|
hfstudio/cli.py
CHANGED
|
@@ -13,6 +13,7 @@ from rich.table import Table
|
|
| 13 |
console = Console()
|
| 14 |
app = typer.Typer(help="HFStudio - Local and API-based Text-to-Speech Studio")
|
| 15 |
|
|
|
|
| 16 |
def get_project_root() -> Path:
|
| 17 |
"""Find the project root directory containing models/"""
|
| 18 |
current_dir = Path(__file__).parent
|
|
@@ -20,70 +21,76 @@ def get_project_root() -> Path:
|
|
| 20 |
if (current_dir / "models").exists():
|
| 21 |
return current_dir
|
| 22 |
current_dir = current_dir.parent
|
| 23 |
-
|
| 24 |
# Fallback to current working directory
|
| 25 |
if (Path.cwd() / "models").exists():
|
| 26 |
return Path.cwd()
|
| 27 |
-
|
| 28 |
raise FileNotFoundError("Could not find models/ directory")
|
| 29 |
|
|
|
|
| 30 |
def get_sample_audio_path() -> str:
|
| 31 |
"""Get path to sample audio file"""
|
| 32 |
project_root = get_project_root()
|
| 33 |
sample_path = project_root / "frontend" / "static" / "samples" / "harvard.wav"
|
| 34 |
if sample_path.exists():
|
| 35 |
return str(sample_path)
|
| 36 |
-
|
| 37 |
# Fallback paths
|
| 38 |
fallback_paths = [
|
| 39 |
project_root / "samples" / "harvard.wav",
|
| 40 |
-
Path.cwd() / "harvard.wav"
|
| 41 |
]
|
| 42 |
-
|
| 43 |
for path in fallback_paths:
|
| 44 |
if path.exists():
|
| 45 |
return str(path)
|
| 46 |
-
|
| 47 |
raise FileNotFoundError("Could not find sample audio file (harvard.wav)")
|
| 48 |
|
|
|
|
| 49 |
def load_model_specs():
|
| 50 |
"""Load model specifications from models/ directory"""
|
| 51 |
project_root = get_project_root()
|
| 52 |
models_dir = project_root / "models"
|
| 53 |
-
|
| 54 |
model_registry = {}
|
| 55 |
-
|
| 56 |
# Scan for model directories with spec.json files
|
| 57 |
for model_dir in models_dir.iterdir():
|
| 58 |
if model_dir.is_dir():
|
| 59 |
spec_file = model_dir / "spec.json"
|
| 60 |
local_script = model_dir / "local.py"
|
| 61 |
-
|
| 62 |
if spec_file.exists():
|
| 63 |
try:
|
| 64 |
-
with open(spec_file,
|
| 65 |
spec = json.load(f)
|
| 66 |
-
|
| 67 |
model_name = model_dir.name
|
| 68 |
model_registry[model_name] = {
|
| 69 |
"script": str(local_script) if local_script.exists() else None,
|
| 70 |
"spec": spec,
|
| 71 |
"description": spec.get("description", ""),
|
| 72 |
-
"status": "Available" if local_script.exists() else "Spec Only"
|
| 73 |
}
|
| 74 |
-
|
| 75 |
# Also register by full model_id if different
|
| 76 |
if spec.get("model_id") and spec["model_id"] != model_name:
|
| 77 |
model_registry[spec["model_id"]] = model_registry[model_name]
|
| 78 |
-
|
| 79 |
except json.JSONDecodeError:
|
| 80 |
-
console.print(
|
| 81 |
-
|
|
|
|
|
|
|
| 82 |
return model_registry
|
| 83 |
|
|
|
|
| 84 |
# Load models dynamically
|
| 85 |
MODEL_REGISTRY = load_model_specs()
|
| 86 |
|
|
|
|
| 87 |
def run_model_script(model_name: str, port: int, host: str):
|
| 88 |
"""Run a model's UV script"""
|
| 89 |
if model_name not in MODEL_REGISTRY:
|
|
@@ -91,39 +98,44 @@ def run_model_script(model_name: str, port: int, host: str):
|
|
| 91 |
console.print(f"[red]Error: Unknown model '{model_name}'[/red]")
|
| 92 |
console.print(f"[yellow]Available models: {available_models}[/yellow]")
|
| 93 |
return False
|
| 94 |
-
|
| 95 |
model_info = MODEL_REGISTRY[model_name]
|
| 96 |
script_path = model_info["script"]
|
| 97 |
-
|
| 98 |
if not script_path:
|
| 99 |
console.print(f"[red]Error: Model '{model_name}' is not yet implemented[/red]")
|
| 100 |
return False
|
| 101 |
-
|
| 102 |
project_root = get_project_root()
|
| 103 |
full_script_path = project_root / script_path
|
| 104 |
-
|
| 105 |
if not full_script_path.exists():
|
| 106 |
console.print(f"[red]Error: Model script not found: {full_script_path}[/red]")
|
| 107 |
return False
|
| 108 |
-
|
| 109 |
try:
|
| 110 |
sample_audio = get_sample_audio_path()
|
| 111 |
except FileNotFoundError as e:
|
| 112 |
console.print(f"[red]Error: {e}[/red]")
|
| 113 |
return False
|
| 114 |
-
|
| 115 |
console.print(f"[green]Starting {model_name} on {host}:{port}[/green]")
|
| 116 |
console.print(f"[dim]Script: {full_script_path}[/dim]")
|
| 117 |
console.print(f"[dim]Sample audio: {sample_audio}[/dim]")
|
| 118 |
-
|
| 119 |
# Run the UV script
|
| 120 |
cmd = [
|
| 121 |
-
"uv",
|
| 122 |
-
"
|
| 123 |
-
|
| 124 |
-
"--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
]
|
| 126 |
-
|
| 127 |
try:
|
| 128 |
subprocess.run(cmd, check=True)
|
| 129 |
except subprocess.CalledProcessError as e:
|
|
@@ -132,24 +144,28 @@ def run_model_script(model_name: str, port: int, host: str):
|
|
| 132 |
except KeyboardInterrupt:
|
| 133 |
console.print("\n[yellow]Model server stopped[/yellow]")
|
| 134 |
return True
|
| 135 |
-
|
| 136 |
return True
|
| 137 |
|
|
|
|
| 138 |
@app.callback(invoke_without_command=True)
|
| 139 |
def main(ctx: typer.Context):
|
| 140 |
"""Welcome to HFStudio CLI"""
|
| 141 |
if ctx.invoked_subcommand is None:
|
| 142 |
-
console.print(
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
@app.command()
|
| 155 |
def start(
|
|
@@ -162,6 +178,7 @@ def start(
|
|
| 162 |
if not success:
|
| 163 |
raise typer.Exit(1)
|
| 164 |
|
|
|
|
| 165 |
@app.command()
|
| 166 |
def list():
|
| 167 |
"""List available TTS models"""
|
|
@@ -169,12 +186,13 @@ def list():
|
|
| 169 |
table.add_column("Model", style="cyan")
|
| 170 |
table.add_column("Description", style="green")
|
| 171 |
table.add_column("Status", style="yellow")
|
| 172 |
-
|
| 173 |
for model_name, model_info in MODEL_REGISTRY.items():
|
| 174 |
table.add_row(model_name, model_info["description"], model_info["status"])
|
| 175 |
-
|
| 176 |
console.print(table)
|
| 177 |
|
|
|
|
| 178 |
@app.command()
|
| 179 |
def dev_server(
|
| 180 |
port: int = typer.Option(7860, "--port", "-p", help="Port to run the server on"),
|
|
@@ -182,22 +200,25 @@ def dev_server(
|
|
| 182 |
dev: bool = typer.Option(False, "--dev", help="Run in development mode"),
|
| 183 |
):
|
| 184 |
"""Start the HFStudio development server"""
|
| 185 |
-
|
| 186 |
-
console.print(
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
| 194 |
uvicorn.run(
|
| 195 |
"hfstudio.server:app",
|
| 196 |
host=host,
|
| 197 |
port=port,
|
| 198 |
reload=dev,
|
| 199 |
-
log_level="info" if not dev else "debug"
|
| 200 |
)
|
| 201 |
|
|
|
|
| 202 |
if __name__ == "__main__":
|
| 203 |
-
app()
|
|
|
|
| 13 |
console = Console()
|
| 14 |
app = typer.Typer(help="HFStudio - Local and API-based Text-to-Speech Studio")
|
| 15 |
|
| 16 |
+
|
| 17 |
def get_project_root() -> Path:
|
| 18 |
"""Find the project root directory containing models/"""
|
| 19 |
current_dir = Path(__file__).parent
|
|
|
|
| 21 |
if (current_dir / "models").exists():
|
| 22 |
return current_dir
|
| 23 |
current_dir = current_dir.parent
|
| 24 |
+
|
| 25 |
# Fallback to current working directory
|
| 26 |
if (Path.cwd() / "models").exists():
|
| 27 |
return Path.cwd()
|
| 28 |
+
|
| 29 |
raise FileNotFoundError("Could not find models/ directory")
|
| 30 |
|
| 31 |
+
|
| 32 |
def get_sample_audio_path() -> str:
|
| 33 |
"""Get path to sample audio file"""
|
| 34 |
project_root = get_project_root()
|
| 35 |
sample_path = project_root / "frontend" / "static" / "samples" / "harvard.wav"
|
| 36 |
if sample_path.exists():
|
| 37 |
return str(sample_path)
|
| 38 |
+
|
| 39 |
# Fallback paths
|
| 40 |
fallback_paths = [
|
| 41 |
project_root / "samples" / "harvard.wav",
|
| 42 |
+
Path.cwd() / "harvard.wav",
|
| 43 |
]
|
| 44 |
+
|
| 45 |
for path in fallback_paths:
|
| 46 |
if path.exists():
|
| 47 |
return str(path)
|
| 48 |
+
|
| 49 |
raise FileNotFoundError("Could not find sample audio file (harvard.wav)")
|
| 50 |
|
| 51 |
+
|
| 52 |
def load_model_specs():
|
| 53 |
"""Load model specifications from models/ directory"""
|
| 54 |
project_root = get_project_root()
|
| 55 |
models_dir = project_root / "models"
|
| 56 |
+
|
| 57 |
model_registry = {}
|
| 58 |
+
|
| 59 |
# Scan for model directories with spec.json files
|
| 60 |
for model_dir in models_dir.iterdir():
|
| 61 |
if model_dir.is_dir():
|
| 62 |
spec_file = model_dir / "spec.json"
|
| 63 |
local_script = model_dir / "local.py"
|
| 64 |
+
|
| 65 |
if spec_file.exists():
|
| 66 |
try:
|
| 67 |
+
with open(spec_file, "r") as f:
|
| 68 |
spec = json.load(f)
|
| 69 |
+
|
| 70 |
model_name = model_dir.name
|
| 71 |
model_registry[model_name] = {
|
| 72 |
"script": str(local_script) if local_script.exists() else None,
|
| 73 |
"spec": spec,
|
| 74 |
"description": spec.get("description", ""),
|
| 75 |
+
"status": "Available" if local_script.exists() else "Spec Only",
|
| 76 |
}
|
| 77 |
+
|
| 78 |
# Also register by full model_id if different
|
| 79 |
if spec.get("model_id") and spec["model_id"] != model_name:
|
| 80 |
model_registry[spec["model_id"]] = model_registry[model_name]
|
| 81 |
+
|
| 82 |
except json.JSONDecodeError:
|
| 83 |
+
console.print(
|
| 84 |
+
f"[yellow]Warning: Invalid JSON in {spec_file}[/yellow]"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
return model_registry
|
| 88 |
|
| 89 |
+
|
| 90 |
# Load models dynamically
|
| 91 |
MODEL_REGISTRY = load_model_specs()
|
| 92 |
|
| 93 |
+
|
| 94 |
def run_model_script(model_name: str, port: int, host: str):
|
| 95 |
"""Run a model's UV script"""
|
| 96 |
if model_name not in MODEL_REGISTRY:
|
|
|
|
| 98 |
console.print(f"[red]Error: Unknown model '{model_name}'[/red]")
|
| 99 |
console.print(f"[yellow]Available models: {available_models}[/yellow]")
|
| 100 |
return False
|
| 101 |
+
|
| 102 |
model_info = MODEL_REGISTRY[model_name]
|
| 103 |
script_path = model_info["script"]
|
| 104 |
+
|
| 105 |
if not script_path:
|
| 106 |
console.print(f"[red]Error: Model '{model_name}' is not yet implemented[/red]")
|
| 107 |
return False
|
| 108 |
+
|
| 109 |
project_root = get_project_root()
|
| 110 |
full_script_path = project_root / script_path
|
| 111 |
+
|
| 112 |
if not full_script_path.exists():
|
| 113 |
console.print(f"[red]Error: Model script not found: {full_script_path}[/red]")
|
| 114 |
return False
|
| 115 |
+
|
| 116 |
try:
|
| 117 |
sample_audio = get_sample_audio_path()
|
| 118 |
except FileNotFoundError as e:
|
| 119 |
console.print(f"[red]Error: {e}[/red]")
|
| 120 |
return False
|
| 121 |
+
|
| 122 |
console.print(f"[green]Starting {model_name} on {host}:{port}[/green]")
|
| 123 |
console.print(f"[dim]Script: {full_script_path}[/dim]")
|
| 124 |
console.print(f"[dim]Sample audio: {sample_audio}[/dim]")
|
| 125 |
+
|
| 126 |
# Run the UV script
|
| 127 |
cmd = [
|
| 128 |
+
"uv",
|
| 129 |
+
"run",
|
| 130 |
+
str(full_script_path),
|
| 131 |
+
"--port",
|
| 132 |
+
str(port),
|
| 133 |
+
"--host",
|
| 134 |
+
host,
|
| 135 |
+
"--sample-audio",
|
| 136 |
+
sample_audio,
|
| 137 |
]
|
| 138 |
+
|
| 139 |
try:
|
| 140 |
subprocess.run(cmd, check=True)
|
| 141 |
except subprocess.CalledProcessError as e:
|
|
|
|
| 144 |
except KeyboardInterrupt:
|
| 145 |
console.print("\n[yellow]Model server stopped[/yellow]")
|
| 146 |
return True
|
| 147 |
+
|
| 148 |
return True
|
| 149 |
|
| 150 |
+
|
| 151 |
@app.callback(invoke_without_command=True)
|
| 152 |
def main(ctx: typer.Context):
|
| 153 |
"""Welcome to HFStudio CLI"""
|
| 154 |
if ctx.invoked_subcommand is None:
|
| 155 |
+
console.print(
|
| 156 |
+
Panel.fit(
|
| 157 |
+
"[bold yellow]ποΈ HFStudio CLI[/bold yellow]\n\n"
|
| 158 |
+
"[green]Available Commands:[/green]\n"
|
| 159 |
+
"β’ [cyan]hfstudio start[/cyan] <model> - Start a TTS model locally\n"
|
| 160 |
+
"β’ [cyan]hfstudio list[/cyan] - List available models\n"
|
| 161 |
+
"β’ [cyan]hfstudio dev-server[/cyan] - Start development server\n"
|
| 162 |
+
"β’ [cyan]hfstudio --help[/cyan] - Show detailed help\n\n"
|
| 163 |
+
"[dim]Example: hfstudio start resambleai/chatterbox --port 1234[/dim]",
|
| 164 |
+
title="ποΈ HFStudio",
|
| 165 |
+
border_style="yellow",
|
| 166 |
+
)
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
|
| 170 |
@app.command()
|
| 171 |
def start(
|
|
|
|
| 178 |
if not success:
|
| 179 |
raise typer.Exit(1)
|
| 180 |
|
| 181 |
+
|
| 182 |
@app.command()
|
| 183 |
def list():
|
| 184 |
"""List available TTS models"""
|
|
|
|
| 186 |
table.add_column("Model", style="cyan")
|
| 187 |
table.add_column("Description", style="green")
|
| 188 |
table.add_column("Status", style="yellow")
|
| 189 |
+
|
| 190 |
for model_name, model_info in MODEL_REGISTRY.items():
|
| 191 |
table.add_row(model_name, model_info["description"], model_info["status"])
|
| 192 |
+
|
| 193 |
console.print(table)
|
| 194 |
|
| 195 |
+
|
| 196 |
@app.command()
|
| 197 |
def dev_server(
|
| 198 |
port: int = typer.Option(7860, "--port", "-p", help="Port to run the server on"),
|
|
|
|
| 200 |
dev: bool = typer.Option(False, "--dev", help="Run in development mode"),
|
| 201 |
):
|
| 202 |
"""Start the HFStudio development server"""
|
| 203 |
+
|
| 204 |
+
console.print(
|
| 205 |
+
Panel.fit(
|
| 206 |
+
"[bold green]HFStudio Development Server[/bold green]\n"
|
| 207 |
+
f"Running on http://{host if host != '0.0.0.0' else 'localhost'}:{port}\n"
|
| 208 |
+
f"API docs: http://localhost:{port}/docs",
|
| 209 |
+
title="ποΈ HFStudio Dev Server",
|
| 210 |
+
border_style="green",
|
| 211 |
+
)
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
uvicorn.run(
|
| 215 |
"hfstudio.server:app",
|
| 216 |
host=host,
|
| 217 |
port=port,
|
| 218 |
reload=dev,
|
| 219 |
+
log_level="info" if not dev else "debug",
|
| 220 |
)
|
| 221 |
|
| 222 |
+
|
| 223 |
if __name__ == "__main__":
|
| 224 |
+
app()
|
hfstudio/server.py
CHANGED
|
@@ -11,9 +11,10 @@ import json
|
|
| 11 |
from pathlib import Path
|
| 12 |
from huggingface_hub import InferenceClient, get_token, whoami
|
| 13 |
|
|
|
|
| 14 |
class TTSRequest(BaseModel):
|
| 15 |
model_config = {"protected_namespaces": ()}
|
| 16 |
-
|
| 17 |
text: str
|
| 18 |
voice_id: str = "default"
|
| 19 |
model_id: str = "coqui-tts"
|
|
@@ -21,6 +22,7 @@ class TTSRequest(BaseModel):
|
|
| 21 |
mode: str = "api"
|
| 22 |
access_token: Optional[str] = None
|
| 23 |
|
|
|
|
| 24 |
class TTSResponse(BaseModel):
|
| 25 |
audio_url: Optional[str] = None
|
| 26 |
duration: Optional[float] = None
|
|
@@ -28,80 +30,86 @@ class TTSResponse(BaseModel):
|
|
| 28 |
error: Optional[str] = None
|
| 29 |
success: bool = True
|
| 30 |
|
|
|
|
| 31 |
app = FastAPI(title="HFStudio API", version="0.1.0")
|
| 32 |
|
| 33 |
static_dir = Path(__file__).parent / "static"
|
| 34 |
models_dir = Path(__file__).parent.parent / "models"
|
| 35 |
|
|
|
|
| 36 |
def load_model_spec(model_id: str) -> Optional[Dict[str, Any]]:
|
| 37 |
"""Load model specification from JSON file."""
|
| 38 |
spec_path = models_dir / model_id / "spec.json"
|
| 39 |
if spec_path.exists():
|
| 40 |
try:
|
| 41 |
-
with open(spec_path,
|
| 42 |
return json.load(f)
|
| 43 |
except (json.JSONDecodeError, IOError):
|
| 44 |
return None
|
| 45 |
return None
|
| 46 |
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
| 48 |
"""Generate TTS using InferenceClient with model specifications."""
|
| 49 |
try:
|
| 50 |
# Build extra_body with parameters from spec
|
| 51 |
extra_body = {}
|
| 52 |
-
|
| 53 |
if request.parameters and "api" in model_spec:
|
| 54 |
api_params = model_spec["api"].get("parameters", {})
|
| 55 |
for param_name, param_value in request.parameters.items():
|
| 56 |
if param_name in api_params:
|
| 57 |
extra_body[param_name] = param_value
|
| 58 |
-
|
| 59 |
# Add voice URL from spec
|
| 60 |
voice_urls = model_spec.get("api", {}).get("voice_urls", {})
|
| 61 |
if request.voice_id.lower() in voice_urls:
|
| 62 |
extra_body["audio_url"] = voice_urls[request.voice_id.lower()]
|
| 63 |
-
|
| 64 |
# Generate audio
|
| 65 |
audio_bytes = client.text_to_speech(
|
| 66 |
request.text,
|
| 67 |
extra_body=extra_body if extra_body else None,
|
| 68 |
)
|
| 69 |
-
|
| 70 |
# Convert to base64 data URL
|
| 71 |
-
audio_base64 = base64.b64encode(audio_bytes).decode(
|
| 72 |
audio_url = f"data:audio/wav;base64,{audio_base64}"
|
| 73 |
-
|
| 74 |
# Estimate duration (simple heuristic)
|
| 75 |
duration = len(request.text) * 0.05
|
| 76 |
-
|
| 77 |
-
return TTSResponse(
|
| 78 |
-
audio_url=audio_url,
|
| 79 |
-
duration=duration,
|
| 80 |
-
format="wav"
|
| 81 |
-
)
|
| 82 |
except Exception as e:
|
| 83 |
error_str = str(e)
|
| 84 |
-
|
| 85 |
if "403 Forbidden" in error_str and "permissions" in error_str:
|
| 86 |
return TTSResponse(
|
| 87 |
success=False,
|
| 88 |
-
error="Your HuggingFace token doesn't have permission to use Inference Providers. Please create a new token with 'Inference API' permissions at https://huggingface.co/settings/tokens"
|
| 89 |
)
|
| 90 |
elif "authentication" in error_str.lower():
|
| 91 |
return TTSResponse(
|
| 92 |
success=False,
|
| 93 |
-
error="Authentication failed. Please check your HuggingFace token or log in again."
|
| 94 |
)
|
| 95 |
else:
|
| 96 |
return TTSResponse(
|
| 97 |
-
success=False,
|
| 98 |
-
error=f"TTS generation error: {error_str}"
|
| 99 |
)
|
|
|
|
|
|
|
| 100 |
if static_dir.exists():
|
| 101 |
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
| 102 |
app.mount("/_app", StaticFiles(directory=str(static_dir / "_app")), name="app")
|
| 103 |
-
app.mount(
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
app.add_middleware(
|
| 107 |
CORSMiddleware,
|
|
@@ -111,26 +119,31 @@ app.add_middleware(
|
|
| 111 |
allow_headers=["*"],
|
| 112 |
)
|
| 113 |
|
|
|
|
| 114 |
class Voice(BaseModel):
|
| 115 |
id: str
|
| 116 |
name: str
|
| 117 |
preview_url: Optional[str] = None
|
| 118 |
supported_models: list[str] = []
|
| 119 |
|
|
|
|
| 120 |
class Model(BaseModel):
|
| 121 |
id: str
|
| 122 |
name: str
|
| 123 |
type: str
|
| 124 |
status: str
|
| 125 |
|
|
|
|
| 126 |
class OAuthTokenRequest(BaseModel):
|
| 127 |
code: str
|
| 128 |
|
|
|
|
| 129 |
class OAuthTokenResponse(BaseModel):
|
| 130 |
access_token: str
|
| 131 |
token_type: str
|
| 132 |
scope: str
|
| 133 |
|
|
|
|
| 134 |
@app.get("/")
|
| 135 |
async def root():
|
| 136 |
index_path = static_dir / "index.html"
|
|
@@ -139,47 +152,50 @@ async def root():
|
|
| 139 |
else:
|
| 140 |
return {"message": "HFStudio API is running"}
|
| 141 |
|
|
|
|
| 142 |
@app.get("/api/status")
|
| 143 |
async def get_status():
|
| 144 |
-
return {
|
| 145 |
-
|
| 146 |
-
"local_available": False,
|
| 147 |
-
"api_configured": True
|
| 148 |
-
}
|
| 149 |
|
| 150 |
@app.get("/api/auth/oauth-config")
|
| 151 |
async def get_oauth_config():
|
| 152 |
-
scopes = os.getenv(
|
| 153 |
-
|
|
|
|
|
|
|
| 154 |
return {
|
| 155 |
-
"client_id": os.getenv(
|
|
|
|
|
|
|
| 156 |
"scopes": scopes,
|
| 157 |
-
"is_spaces": bool(os.getenv("SPACE_HOST"))
|
| 158 |
}
|
| 159 |
|
|
|
|
| 160 |
@app.get("/api/auth/local-token")
|
| 161 |
async def get_local_token():
|
| 162 |
try:
|
| 163 |
if os.getenv("SPACE_HOST"):
|
| 164 |
return {"available": False, "reason": "running_on_spaces"}
|
| 165 |
-
|
| 166 |
token = get_token()
|
| 167 |
if not token:
|
| 168 |
return {"available": False, "reason": "no_local_token"}
|
| 169 |
-
|
| 170 |
try:
|
| 171 |
user_info = whoami(token=token)
|
| 172 |
if user_info.get("type") != "user":
|
| 173 |
return {"available": False, "reason": "invalid_token_type"}
|
| 174 |
-
|
| 175 |
return {
|
| 176 |
"available": True,
|
| 177 |
"token": token,
|
| 178 |
"user_info": {
|
| 179 |
"name": user_info.get("name"),
|
| 180 |
"fullname": user_info.get("fullname"),
|
| 181 |
-
"avatarUrl": user_info.get("avatarUrl")
|
| 182 |
-
}
|
| 183 |
}
|
| 184 |
except Exception as api_error:
|
| 185 |
if "429" in str(api_error) or "rate limit" in str(api_error).lower():
|
|
@@ -189,20 +205,24 @@ async def get_local_token():
|
|
| 189 |
"user_info": {
|
| 190 |
"name": "Local User",
|
| 191 |
"fullname": "Local User",
|
| 192 |
-
"avatarUrl": None
|
| 193 |
},
|
| 194 |
-
"warning": "Token validation skipped due to rate limiting"
|
| 195 |
}
|
| 196 |
else:
|
| 197 |
-
return {
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
| 199 |
except Exception as e:
|
| 200 |
return {"available": False, "reason": f"error: {str(e)}"}
|
| 201 |
|
|
|
|
| 202 |
@app.get("/api/voices")
|
| 203 |
async def get_voices():
|
| 204 |
voices = []
|
| 205 |
-
|
| 206 |
# Load voices from all model specifications
|
| 207 |
for model_dir in models_dir.iterdir():
|
| 208 |
if model_dir.is_dir():
|
|
@@ -212,17 +232,20 @@ async def get_voices():
|
|
| 212 |
voice = Voice(
|
| 213 |
id=voice_spec["id"],
|
| 214 |
name=voice_spec["name"],
|
| 215 |
-
preview_url=model_spec.get("api", {})
|
| 216 |
-
|
|
|
|
|
|
|
| 217 |
)
|
| 218 |
voices.append(voice)
|
| 219 |
-
|
| 220 |
return {"voices": voices}
|
| 221 |
|
|
|
|
| 222 |
@app.get("/api/models")
|
| 223 |
async def get_models():
|
| 224 |
models = []
|
| 225 |
-
|
| 226 |
# Load models from specifications
|
| 227 |
for model_dir in models_dir.iterdir():
|
| 228 |
if model_dir.is_dir():
|
|
@@ -230,17 +253,18 @@ async def get_models():
|
|
| 230 |
if model_spec:
|
| 231 |
model_type = "api" if "api" in model_spec else "local"
|
| 232 |
status = "available" if model_type == "api" else "downloadable"
|
| 233 |
-
|
| 234 |
model = Model(
|
| 235 |
id=model_dir.name,
|
| 236 |
name=model_spec.get("name", model_dir.name),
|
| 237 |
type=model_type,
|
| 238 |
-
status=status
|
| 239 |
)
|
| 240 |
models.append(model)
|
| 241 |
-
|
| 242 |
return {"models": models}
|
| 243 |
|
|
|
|
| 244 |
@app.post("/api/tts/generate")
|
| 245 |
async def generate_tts(request: TTSRequest):
|
| 246 |
try:
|
|
@@ -249,19 +273,20 @@ async def generate_tts(request: TTSRequest):
|
|
| 249 |
if not model_spec:
|
| 250 |
return TTSResponse(
|
| 251 |
success=False,
|
| 252 |
-
error=f"Model specification not found for {request.model_id}"
|
| 253 |
)
|
| 254 |
-
|
| 255 |
# Create client based on mode
|
| 256 |
if request.mode == "api":
|
| 257 |
if not request.access_token:
|
| 258 |
return TTSResponse(
|
| 259 |
-
success=False,
|
| 260 |
-
error="Please log in to HuggingFace to use the API."
|
| 261 |
)
|
| 262 |
-
|
| 263 |
# Get model endpoint from spec
|
| 264 |
-
endpoint_model = model_spec.get("api", {}).get(
|
|
|
|
|
|
|
| 265 |
client = InferenceClient(
|
| 266 |
api_key=request.access_token,
|
| 267 |
model=endpoint_model,
|
|
@@ -273,30 +298,32 @@ async def generate_tts(request: TTSRequest):
|
|
| 273 |
client = InferenceClient(base_url=f"http://localhost:{port}/v1/")
|
| 274 |
else:
|
| 275 |
return TTSResponse(
|
| 276 |
-
success=False,
|
| 277 |
-
error="Invalid mode. Use 'api' or 'local'."
|
| 278 |
)
|
| 279 |
-
|
| 280 |
# Generate TTS using the unified helper function
|
| 281 |
result = generate_tts_with_client(client, request, model_spec)
|
| 282 |
-
|
| 283 |
# Add specific error handling for local mode
|
| 284 |
if not result.success and request.mode == "local":
|
| 285 |
result.error = f"Local server error: {result.error}. Make sure to run 'hfstudio start {request.model_id}' first."
|
| 286 |
-
|
| 287 |
return result
|
| 288 |
-
|
| 289 |
except Exception as e:
|
| 290 |
raise HTTPException(status_code=500, detail=str(e))
|
| 291 |
|
|
|
|
| 292 |
@app.post("/api/auth/token")
|
| 293 |
async def exchange_oauth_token(request: OAuthTokenRequest, http_request: Request):
|
| 294 |
try:
|
| 295 |
token_url = "https://huggingface.co/oauth/token"
|
| 296 |
-
|
| 297 |
client_id = os.getenv("OAUTH_CLIENT_ID", "cdf32a17-e40f-4a84-b683-f66aa1105793")
|
| 298 |
-
client_secret = os.getenv(
|
| 299 |
-
|
|
|
|
|
|
|
| 300 |
if os.getenv("SPACE_HOST"):
|
| 301 |
space_host = os.getenv("SPACE_HOST").split(",")[0]
|
| 302 |
redirect_uri = f"https://{space_host}/auth/callback"
|
|
@@ -304,11 +331,12 @@ async def exchange_oauth_token(request: OAuthTokenRequest, http_request: Request
|
|
| 304 |
referer = http_request.headers.get("referer", "")
|
| 305 |
if referer:
|
| 306 |
from urllib.parse import urlparse
|
|
|
|
| 307 |
parsed = urlparse(referer)
|
| 308 |
redirect_uri = f"{parsed.scheme}://{parsed.netloc}/auth/callback"
|
| 309 |
else:
|
| 310 |
redirect_uri = "http://localhost:7860/auth/callback"
|
| 311 |
-
|
| 312 |
token_data = {
|
| 313 |
"client_id": client_id,
|
| 314 |
"client_secret": client_secret,
|
|
@@ -316,37 +344,36 @@ async def exchange_oauth_token(request: OAuthTokenRequest, http_request: Request
|
|
| 316 |
"grant_type": "authorization_code",
|
| 317 |
"redirect_uri": redirect_uri,
|
| 318 |
}
|
| 319 |
-
|
| 320 |
async with httpx.AsyncClient() as client:
|
| 321 |
response = await client.post(
|
| 322 |
-
token_url,
|
| 323 |
-
data=token_data,
|
| 324 |
-
headers={"Accept": "application/json"}
|
| 325 |
)
|
| 326 |
-
|
| 327 |
if response.status_code != 200:
|
| 328 |
raise HTTPException(
|
| 329 |
-
status_code=400,
|
| 330 |
-
detail=f"Token exchange failed: {response.text}"
|
| 331 |
)
|
| 332 |
-
|
| 333 |
token_response = response.json()
|
| 334 |
-
|
| 335 |
return OAuthTokenResponse(
|
| 336 |
access_token=token_response["access_token"],
|
| 337 |
token_type=token_response.get("token_type", "Bearer"),
|
| 338 |
-
scope=token_response.get("scope", "")
|
| 339 |
)
|
| 340 |
-
|
| 341 |
except httpx.RequestError as e:
|
| 342 |
raise HTTPException(status_code=500, detail=f"Network error: {str(e)}")
|
| 343 |
except Exception as e:
|
| 344 |
raise HTTPException(status_code=500, detail=str(e))
|
| 345 |
|
|
|
|
| 346 |
@app.get("/auth/callback")
|
| 347 |
async def oauth_callback(code: str = None, state: str = None, request: Request = None):
|
| 348 |
if not code:
|
| 349 |
-
return HTMLResponse(
|
|
|
|
| 350 |
<html>
|
| 351 |
<head><title>OAuth Error</title></head>
|
| 352 |
<body>
|
|
@@ -355,20 +382,24 @@ async def oauth_callback(code: str = None, state: str = None, request: Request =
|
|
| 355 |
<script>window.close();</script>
|
| 356 |
</body>
|
| 357 |
</html>
|
| 358 |
-
""",
|
| 359 |
-
|
|
|
|
|
|
|
| 360 |
try:
|
| 361 |
token_url = "https://huggingface.co/oauth/token"
|
| 362 |
-
|
| 363 |
client_id = os.getenv("OAUTH_CLIENT_ID", "cdf32a17-e40f-4a84-b683-f66aa1105793")
|
| 364 |
-
client_secret = os.getenv(
|
| 365 |
-
|
|
|
|
|
|
|
| 366 |
if os.getenv("SPACE_HOST"):
|
| 367 |
space_host = os.getenv("SPACE_HOST").split(",")[0]
|
| 368 |
redirect_uri = f"https://{space_host}/auth/callback"
|
| 369 |
else:
|
| 370 |
redirect_uri = "http://localhost:7860/auth/callback"
|
| 371 |
-
|
| 372 |
token_data = {
|
| 373 |
"client_id": client_id,
|
| 374 |
"client_secret": client_secret,
|
|
@@ -376,18 +407,16 @@ async def oauth_callback(code: str = None, state: str = None, request: Request =
|
|
| 376 |
"grant_type": "authorization_code",
|
| 377 |
"redirect_uri": redirect_uri,
|
| 378 |
}
|
| 379 |
-
|
| 380 |
async with httpx.AsyncClient() as client:
|
| 381 |
response = await client.post(
|
| 382 |
-
token_url,
|
| 383 |
-
data=token_data,
|
| 384 |
-
headers={"Accept": "application/json"}
|
| 385 |
)
|
| 386 |
-
|
| 387 |
if response.status_code == 200:
|
| 388 |
token_response = response.json()
|
| 389 |
access_token = token_response["access_token"]
|
| 390 |
-
|
| 391 |
return HTMLResponse(f"""
|
| 392 |
<html>
|
| 393 |
<head><title>OAuth Success</title></head>
|
|
@@ -402,7 +431,8 @@ async def oauth_callback(code: str = None, state: str = None, request: Request =
|
|
| 402 |
</html>
|
| 403 |
""")
|
| 404 |
else:
|
| 405 |
-
return HTMLResponse(
|
|
|
|
| 406 |
<html>
|
| 407 |
<head><title>OAuth Error</title></head>
|
| 408 |
<body>
|
|
@@ -411,10 +441,13 @@ async def oauth_callback(code: str = None, state: str = None, request: Request =
|
|
| 411 |
<a href="/">Return to app</a>
|
| 412 |
</body>
|
| 413 |
</html>
|
| 414 |
-
""",
|
| 415 |
-
|
|
|
|
|
|
|
| 416 |
except Exception as e:
|
| 417 |
-
return HTMLResponse(
|
|
|
|
| 418 |
<html>
|
| 419 |
<head><title>OAuth Error</title></head>
|
| 420 |
<body>
|
|
@@ -423,13 +456,20 @@ async def oauth_callback(code: str = None, state: str = None, request: Request =
|
|
| 423 |
<a href="/">Return to app</a>
|
| 424 |
</body>
|
| 425 |
</html>
|
| 426 |
-
""",
|
|
|
|
|
|
|
|
|
|
| 427 |
|
| 428 |
@app.get("/{path:path}")
|
| 429 |
async def serve_spa(path: str):
|
| 430 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
raise HTTPException(status_code=404, detail="Not found")
|
| 432 |
-
|
| 433 |
index_path = static_dir / "index.html"
|
| 434 |
if index_path.exists():
|
| 435 |
return FileResponse(str(index_path), media_type="text/html")
|
|
@@ -445,6 +485,8 @@ async def serve_spa(path: str):
|
|
| 445 |
</html>
|
| 446 |
""")
|
| 447 |
|
|
|
|
| 448 |
if __name__ == "__main__":
|
| 449 |
import uvicorn
|
| 450 |
-
|
|
|
|
|
|
| 11 |
from pathlib import Path
|
| 12 |
from huggingface_hub import InferenceClient, get_token, whoami
|
| 13 |
|
| 14 |
+
|
| 15 |
class TTSRequest(BaseModel):
|
| 16 |
model_config = {"protected_namespaces": ()}
|
| 17 |
+
|
| 18 |
text: str
|
| 19 |
voice_id: str = "default"
|
| 20 |
model_id: str = "coqui-tts"
|
|
|
|
| 22 |
mode: str = "api"
|
| 23 |
access_token: Optional[str] = None
|
| 24 |
|
| 25 |
+
|
| 26 |
class TTSResponse(BaseModel):
|
| 27 |
audio_url: Optional[str] = None
|
| 28 |
duration: Optional[float] = None
|
|
|
|
| 30 |
error: Optional[str] = None
|
| 31 |
success: bool = True
|
| 32 |
|
| 33 |
+
|
| 34 |
app = FastAPI(title="HFStudio API", version="0.1.0")
|
| 35 |
|
| 36 |
static_dir = Path(__file__).parent / "static"
|
| 37 |
models_dir = Path(__file__).parent.parent / "models"
|
| 38 |
|
| 39 |
+
|
| 40 |
def load_model_spec(model_id: str) -> Optional[Dict[str, Any]]:
|
| 41 |
"""Load model specification from JSON file."""
|
| 42 |
spec_path = models_dir / model_id / "spec.json"
|
| 43 |
if spec_path.exists():
|
| 44 |
try:
|
| 45 |
+
with open(spec_path, "r") as f:
|
| 46 |
return json.load(f)
|
| 47 |
except (json.JSONDecodeError, IOError):
|
| 48 |
return None
|
| 49 |
return None
|
| 50 |
|
| 51 |
+
|
| 52 |
+
def generate_tts_with_client(
|
| 53 |
+
client: InferenceClient, request: TTSRequest, model_spec: Dict[str, Any]
|
| 54 |
+
) -> TTSResponse:
|
| 55 |
"""Generate TTS using InferenceClient with model specifications."""
|
| 56 |
try:
|
| 57 |
# Build extra_body with parameters from spec
|
| 58 |
extra_body = {}
|
| 59 |
+
|
| 60 |
if request.parameters and "api" in model_spec:
|
| 61 |
api_params = model_spec["api"].get("parameters", {})
|
| 62 |
for param_name, param_value in request.parameters.items():
|
| 63 |
if param_name in api_params:
|
| 64 |
extra_body[param_name] = param_value
|
| 65 |
+
|
| 66 |
# Add voice URL from spec
|
| 67 |
voice_urls = model_spec.get("api", {}).get("voice_urls", {})
|
| 68 |
if request.voice_id.lower() in voice_urls:
|
| 69 |
extra_body["audio_url"] = voice_urls[request.voice_id.lower()]
|
| 70 |
+
|
| 71 |
# Generate audio
|
| 72 |
audio_bytes = client.text_to_speech(
|
| 73 |
request.text,
|
| 74 |
extra_body=extra_body if extra_body else None,
|
| 75 |
)
|
| 76 |
+
|
| 77 |
# Convert to base64 data URL
|
| 78 |
+
audio_base64 = base64.b64encode(audio_bytes).decode("utf-8")
|
| 79 |
audio_url = f"data:audio/wav;base64,{audio_base64}"
|
| 80 |
+
|
| 81 |
# Estimate duration (simple heuristic)
|
| 82 |
duration = len(request.text) * 0.05
|
| 83 |
+
|
| 84 |
+
return TTSResponse(audio_url=audio_url, duration=duration, format="wav")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
except Exception as e:
|
| 86 |
error_str = str(e)
|
| 87 |
+
|
| 88 |
if "403 Forbidden" in error_str and "permissions" in error_str:
|
| 89 |
return TTSResponse(
|
| 90 |
success=False,
|
| 91 |
+
error="Your HuggingFace token doesn't have permission to use Inference Providers. Please create a new token with 'Inference API' permissions at https://huggingface.co/settings/tokens",
|
| 92 |
)
|
| 93 |
elif "authentication" in error_str.lower():
|
| 94 |
return TTSResponse(
|
| 95 |
success=False,
|
| 96 |
+
error="Authentication failed. Please check your HuggingFace token or log in again.",
|
| 97 |
)
|
| 98 |
else:
|
| 99 |
return TTSResponse(
|
| 100 |
+
success=False, error=f"TTS generation error: {error_str}"
|
|
|
|
| 101 |
)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
if static_dir.exists():
|
| 105 |
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
| 106 |
app.mount("/_app", StaticFiles(directory=str(static_dir / "_app")), name="app")
|
| 107 |
+
app.mount(
|
| 108 |
+
"/assets", StaticFiles(directory=str(static_dir / "assets")), name="assets"
|
| 109 |
+
)
|
| 110 |
+
app.mount(
|
| 111 |
+
"/samples", StaticFiles(directory=str(static_dir / "samples")), name="samples"
|
| 112 |
+
)
|
| 113 |
|
| 114 |
app.add_middleware(
|
| 115 |
CORSMiddleware,
|
|
|
|
| 119 |
allow_headers=["*"],
|
| 120 |
)
|
| 121 |
|
| 122 |
+
|
| 123 |
class Voice(BaseModel):
|
| 124 |
id: str
|
| 125 |
name: str
|
| 126 |
preview_url: Optional[str] = None
|
| 127 |
supported_models: list[str] = []
|
| 128 |
|
| 129 |
+
|
| 130 |
class Model(BaseModel):
|
| 131 |
id: str
|
| 132 |
name: str
|
| 133 |
type: str
|
| 134 |
status: str
|
| 135 |
|
| 136 |
+
|
| 137 |
class OAuthTokenRequest(BaseModel):
|
| 138 |
code: str
|
| 139 |
|
| 140 |
+
|
| 141 |
class OAuthTokenResponse(BaseModel):
|
| 142 |
access_token: str
|
| 143 |
token_type: str
|
| 144 |
scope: str
|
| 145 |
|
| 146 |
+
|
| 147 |
@app.get("/")
|
| 148 |
async def root():
|
| 149 |
index_path = static_dir / "index.html"
|
|
|
|
| 152 |
else:
|
| 153 |
return {"message": "HFStudio API is running"}
|
| 154 |
|
| 155 |
+
|
| 156 |
@app.get("/api/status")
|
| 157 |
async def get_status():
|
| 158 |
+
return {"mode": "api", "local_available": False, "api_configured": True}
|
| 159 |
+
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
@app.get("/api/auth/oauth-config")
|
| 162 |
async def get_oauth_config():
|
| 163 |
+
scopes = os.getenv(
|
| 164 |
+
"OAUTH_SCOPES", "read-repos write-repos manage-repos inference-api"
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
return {
|
| 168 |
+
"client_id": os.getenv(
|
| 169 |
+
"OAUTH_CLIENT_ID", "cdf32a17-e40f-4a84-b683-f66aa1105793"
|
| 170 |
+
),
|
| 171 |
"scopes": scopes,
|
| 172 |
+
"is_spaces": bool(os.getenv("SPACE_HOST")),
|
| 173 |
}
|
| 174 |
|
| 175 |
+
|
| 176 |
@app.get("/api/auth/local-token")
|
| 177 |
async def get_local_token():
|
| 178 |
try:
|
| 179 |
if os.getenv("SPACE_HOST"):
|
| 180 |
return {"available": False, "reason": "running_on_spaces"}
|
| 181 |
+
|
| 182 |
token = get_token()
|
| 183 |
if not token:
|
| 184 |
return {"available": False, "reason": "no_local_token"}
|
| 185 |
+
|
| 186 |
try:
|
| 187 |
user_info = whoami(token=token)
|
| 188 |
if user_info.get("type") != "user":
|
| 189 |
return {"available": False, "reason": "invalid_token_type"}
|
| 190 |
+
|
| 191 |
return {
|
| 192 |
"available": True,
|
| 193 |
"token": token,
|
| 194 |
"user_info": {
|
| 195 |
"name": user_info.get("name"),
|
| 196 |
"fullname": user_info.get("fullname"),
|
| 197 |
+
"avatarUrl": user_info.get("avatarUrl"),
|
| 198 |
+
},
|
| 199 |
}
|
| 200 |
except Exception as api_error:
|
| 201 |
if "429" in str(api_error) or "rate limit" in str(api_error).lower():
|
|
|
|
| 205 |
"user_info": {
|
| 206 |
"name": "Local User",
|
| 207 |
"fullname": "Local User",
|
| 208 |
+
"avatarUrl": None,
|
| 209 |
},
|
| 210 |
+
"warning": "Token validation skipped due to rate limiting",
|
| 211 |
}
|
| 212 |
else:
|
| 213 |
+
return {
|
| 214 |
+
"available": False,
|
| 215 |
+
"reason": f"token_validation_error: {str(api_error)}",
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
except Exception as e:
|
| 219 |
return {"available": False, "reason": f"error: {str(e)}"}
|
| 220 |
|
| 221 |
+
|
| 222 |
@app.get("/api/voices")
|
| 223 |
async def get_voices():
|
| 224 |
voices = []
|
| 225 |
+
|
| 226 |
# Load voices from all model specifications
|
| 227 |
for model_dir in models_dir.iterdir():
|
| 228 |
if model_dir.is_dir():
|
|
|
|
| 232 |
voice = Voice(
|
| 233 |
id=voice_spec["id"],
|
| 234 |
name=voice_spec["name"],
|
| 235 |
+
preview_url=model_spec.get("api", {})
|
| 236 |
+
.get("voice_urls", {})
|
| 237 |
+
.get(voice_spec["id"]),
|
| 238 |
+
supported_models=[model_dir.name],
|
| 239 |
)
|
| 240 |
voices.append(voice)
|
| 241 |
+
|
| 242 |
return {"voices": voices}
|
| 243 |
|
| 244 |
+
|
| 245 |
@app.get("/api/models")
|
| 246 |
async def get_models():
|
| 247 |
models = []
|
| 248 |
+
|
| 249 |
# Load models from specifications
|
| 250 |
for model_dir in models_dir.iterdir():
|
| 251 |
if model_dir.is_dir():
|
|
|
|
| 253 |
if model_spec:
|
| 254 |
model_type = "api" if "api" in model_spec else "local"
|
| 255 |
status = "available" if model_type == "api" else "downloadable"
|
| 256 |
+
|
| 257 |
model = Model(
|
| 258 |
id=model_dir.name,
|
| 259 |
name=model_spec.get("name", model_dir.name),
|
| 260 |
type=model_type,
|
| 261 |
+
status=status,
|
| 262 |
)
|
| 263 |
models.append(model)
|
| 264 |
+
|
| 265 |
return {"models": models}
|
| 266 |
|
| 267 |
+
|
| 268 |
@app.post("/api/tts/generate")
|
| 269 |
async def generate_tts(request: TTSRequest):
|
| 270 |
try:
|
|
|
|
| 273 |
if not model_spec:
|
| 274 |
return TTSResponse(
|
| 275 |
success=False,
|
| 276 |
+
error=f"Model specification not found for {request.model_id}",
|
| 277 |
)
|
| 278 |
+
|
| 279 |
# Create client based on mode
|
| 280 |
if request.mode == "api":
|
| 281 |
if not request.access_token:
|
| 282 |
return TTSResponse(
|
| 283 |
+
success=False, error="Please log in to HuggingFace to use the API."
|
|
|
|
| 284 |
)
|
| 285 |
+
|
| 286 |
# Get model endpoint from spec
|
| 287 |
+
endpoint_model = model_spec.get("api", {}).get(
|
| 288 |
+
"endpoint_model", request.model_id
|
| 289 |
+
)
|
| 290 |
client = InferenceClient(
|
| 291 |
api_key=request.access_token,
|
| 292 |
model=endpoint_model,
|
|
|
|
| 298 |
client = InferenceClient(base_url=f"http://localhost:{port}/v1/")
|
| 299 |
else:
|
| 300 |
return TTSResponse(
|
| 301 |
+
success=False, error="Invalid mode. Use 'api' or 'local'."
|
|
|
|
| 302 |
)
|
| 303 |
+
|
| 304 |
# Generate TTS using the unified helper function
|
| 305 |
result = generate_tts_with_client(client, request, model_spec)
|
| 306 |
+
|
| 307 |
# Add specific error handling for local mode
|
| 308 |
if not result.success and request.mode == "local":
|
| 309 |
result.error = f"Local server error: {result.error}. Make sure to run 'hfstudio start {request.model_id}' first."
|
| 310 |
+
|
| 311 |
return result
|
| 312 |
+
|
| 313 |
except Exception as e:
|
| 314 |
raise HTTPException(status_code=500, detail=str(e))
|
| 315 |
|
| 316 |
+
|
| 317 |
@app.post("/api/auth/token")
|
| 318 |
async def exchange_oauth_token(request: OAuthTokenRequest, http_request: Request):
|
| 319 |
try:
|
| 320 |
token_url = "https://huggingface.co/oauth/token"
|
| 321 |
+
|
| 322 |
client_id = os.getenv("OAUTH_CLIENT_ID", "cdf32a17-e40f-4a84-b683-f66aa1105793")
|
| 323 |
+
client_secret = os.getenv(
|
| 324 |
+
"OAUTH_CLIENT_SECRET", "f590cb2d-6eac-4cef-a0cb-d0116825295c"
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
if os.getenv("SPACE_HOST"):
|
| 328 |
space_host = os.getenv("SPACE_HOST").split(",")[0]
|
| 329 |
redirect_uri = f"https://{space_host}/auth/callback"
|
|
|
|
| 331 |
referer = http_request.headers.get("referer", "")
|
| 332 |
if referer:
|
| 333 |
from urllib.parse import urlparse
|
| 334 |
+
|
| 335 |
parsed = urlparse(referer)
|
| 336 |
redirect_uri = f"{parsed.scheme}://{parsed.netloc}/auth/callback"
|
| 337 |
else:
|
| 338 |
redirect_uri = "http://localhost:7860/auth/callback"
|
| 339 |
+
|
| 340 |
token_data = {
|
| 341 |
"client_id": client_id,
|
| 342 |
"client_secret": client_secret,
|
|
|
|
| 344 |
"grant_type": "authorization_code",
|
| 345 |
"redirect_uri": redirect_uri,
|
| 346 |
}
|
| 347 |
+
|
| 348 |
async with httpx.AsyncClient() as client:
|
| 349 |
response = await client.post(
|
| 350 |
+
token_url, data=token_data, headers={"Accept": "application/json"}
|
|
|
|
|
|
|
| 351 |
)
|
| 352 |
+
|
| 353 |
if response.status_code != 200:
|
| 354 |
raise HTTPException(
|
| 355 |
+
status_code=400, detail=f"Token exchange failed: {response.text}"
|
|
|
|
| 356 |
)
|
| 357 |
+
|
| 358 |
token_response = response.json()
|
| 359 |
+
|
| 360 |
return OAuthTokenResponse(
|
| 361 |
access_token=token_response["access_token"],
|
| 362 |
token_type=token_response.get("token_type", "Bearer"),
|
| 363 |
+
scope=token_response.get("scope", ""),
|
| 364 |
)
|
| 365 |
+
|
| 366 |
except httpx.RequestError as e:
|
| 367 |
raise HTTPException(status_code=500, detail=f"Network error: {str(e)}")
|
| 368 |
except Exception as e:
|
| 369 |
raise HTTPException(status_code=500, detail=str(e))
|
| 370 |
|
| 371 |
+
|
| 372 |
@app.get("/auth/callback")
|
| 373 |
async def oauth_callback(code: str = None, state: str = None, request: Request = None):
|
| 374 |
if not code:
|
| 375 |
+
return HTMLResponse(
|
| 376 |
+
"""
|
| 377 |
<html>
|
| 378 |
<head><title>OAuth Error</title></head>
|
| 379 |
<body>
|
|
|
|
| 382 |
<script>window.close();</script>
|
| 383 |
</body>
|
| 384 |
</html>
|
| 385 |
+
""",
|
| 386 |
+
status_code=400,
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
try:
|
| 390 |
token_url = "https://huggingface.co/oauth/token"
|
| 391 |
+
|
| 392 |
client_id = os.getenv("OAUTH_CLIENT_ID", "cdf32a17-e40f-4a84-b683-f66aa1105793")
|
| 393 |
+
client_secret = os.getenv(
|
| 394 |
+
"OAUTH_CLIENT_SECRET", "f590cb2d-6eac-4cef-a0cb-d0116825295c"
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
if os.getenv("SPACE_HOST"):
|
| 398 |
space_host = os.getenv("SPACE_HOST").split(",")[0]
|
| 399 |
redirect_uri = f"https://{space_host}/auth/callback"
|
| 400 |
else:
|
| 401 |
redirect_uri = "http://localhost:7860/auth/callback"
|
| 402 |
+
|
| 403 |
token_data = {
|
| 404 |
"client_id": client_id,
|
| 405 |
"client_secret": client_secret,
|
|
|
|
| 407 |
"grant_type": "authorization_code",
|
| 408 |
"redirect_uri": redirect_uri,
|
| 409 |
}
|
| 410 |
+
|
| 411 |
async with httpx.AsyncClient() as client:
|
| 412 |
response = await client.post(
|
| 413 |
+
token_url, data=token_data, headers={"Accept": "application/json"}
|
|
|
|
|
|
|
| 414 |
)
|
| 415 |
+
|
| 416 |
if response.status_code == 200:
|
| 417 |
token_response = response.json()
|
| 418 |
access_token = token_response["access_token"]
|
| 419 |
+
|
| 420 |
return HTMLResponse(f"""
|
| 421 |
<html>
|
| 422 |
<head><title>OAuth Success</title></head>
|
|
|
|
| 431 |
</html>
|
| 432 |
""")
|
| 433 |
else:
|
| 434 |
+
return HTMLResponse(
|
| 435 |
+
f"""
|
| 436 |
<html>
|
| 437 |
<head><title>OAuth Error</title></head>
|
| 438 |
<body>
|
|
|
|
| 441 |
<a href="/">Return to app</a>
|
| 442 |
</body>
|
| 443 |
</html>
|
| 444 |
+
""",
|
| 445 |
+
status_code=400,
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
except Exception as e:
|
| 449 |
+
return HTMLResponse(
|
| 450 |
+
f"""
|
| 451 |
<html>
|
| 452 |
<head><title>OAuth Error</title></head>
|
| 453 |
<body>
|
|
|
|
| 456 |
<a href="/">Return to app</a>
|
| 457 |
</body>
|
| 458 |
</html>
|
| 459 |
+
""",
|
| 460 |
+
status_code=500,
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
|
| 464 |
@app.get("/{path:path}")
|
| 465 |
async def serve_spa(path: str):
|
| 466 |
+
if (
|
| 467 |
+
path.startswith("api/")
|
| 468 |
+
or path.startswith("docs")
|
| 469 |
+
or path.startswith("openapi.json")
|
| 470 |
+
):
|
| 471 |
raise HTTPException(status_code=404, detail="Not found")
|
| 472 |
+
|
| 473 |
index_path = static_dir / "index.html"
|
| 474 |
if index_path.exists():
|
| 475 |
return FileResponse(str(index_path), media_type="text/html")
|
|
|
|
| 485 |
</html>
|
| 486 |
""")
|
| 487 |
|
| 488 |
+
|
| 489 |
if __name__ == "__main__":
|
| 490 |
import uvicorn
|
| 491 |
+
|
| 492 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
hfstudio/static/_app/immutable/assets/2.D7LovqyU.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@keyframes svelte-1sw3914-fade-in{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.animate-fade-in.svelte-1sw3914{animation:svelte-1sw3914-fade-in .3s ease-out}@keyframes svelte-1sw3914-sweep{0%{left:-100%}20%{left:-100%}80%{left:100%}to{left:100%}}.flash-sweep.svelte-1sw3914{position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent 0%,rgba(251,191,36,.5) 25%,rgba(249,115,22,.8) 50%,rgba(251,191,36,.5) 75%,transparent 100%);animation:svelte-1sw3914-sweep 2s ease-in-out;pointer-events:none}.code-flash.svelte-1sw3914{animation:svelte-1sw3914-pulse .5s ease-out}@keyframes svelte-1sw3914-pulse{0%{box-shadow:0 0 #fbbf2400}50%{box-shadow:0 0 0 6px #fbbf2466}to{box-shadow:0 0 #fbbf2400}}
|
hfstudio/static/_app/immutable/chunks/DVK2ASb7.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
var St=Object.defineProperty;var kt=(e,t,n)=>t in e?St(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var R=(e,t,n)=>kt(e,typeof t!="symbol"?t+"":t,n);import{S as Et,F as At,Z as Rt,_ as Tt,$ as It,a0 as Ut,a1 as Lt,a2 as $t,v as ve,a3 as xt,G as be,A as ge,s as Ct}from"./BF3xBGh8.js";class Ze extends Et{constructor(n){if(!n||!n.target&&!n.$$inline)throw new Error("'target' is a required option");super();R(this,"$$prop_def");R(this,"$$events_def");R(this,"$$slot_def")}$destroy(){super.$destroy(),this.$destroy=()=>{console.warn("Component was already destroyed")}}$capture_state(){}$inject_state(){}}class Pt extends Ze{}const Ot=Object.freeze(Object.defineProperty({__proto__:null,SvelteComponent:Ze,SvelteComponentTyped:Pt,afterUpdate:At,beforeUpdate:Rt,createEventDispatcher:Tt,getAllContexts:It,getContext:Ut,hasContext:Lt,onDestroy:$t,onMount:ve,setContext:xt,tick:be},Symbol.toStringTag,{value:"Module"}));class ie{constructor(t,n){this.status=t,typeof n=="string"?this.body={message:n}:n?this.body=n:this.body={message:`Error: ${t}`}}toString(){return JSON.stringify(this.body)}}class Re{constructor(t,n){this.status=t,this.location=n}}class Te extends Error{constructor(t,n,r){super(r),this.status=t,this.text=n}}new URL("sveltekit-internal://");function Nt(e,t){return e==="/"||t==="ignore"?e:t==="never"?e.endsWith("/")?e.slice(0,-1):e:t==="always"&&!e.endsWith("/")?e+"/":e}function jt(e){return e.split("%25").map(decodeURI).join("%25")}function Dt(e){for(const t in e)e[t]=decodeURIComponent(e[t]);return e}function me({href:e}){return e.split("#")[0]}function Ft(e,t,n,r=!1){const a=new URL(e);Object.defineProperty(a,"searchParams",{value:new Proxy(a.searchParams,{get(i,o){if(o==="get"||o==="getAll"||o==="has")return l=>(n(l),i[o](l));t();const c=Reflect.get(i,o);return typeof c=="function"?c.bind(i):c}}),enumerable:!0,configurable:!0});const s=["href","pathname","search","toString","toJSON"];r&&s.push("hash");for(const i of s)Object.defineProperty(a,i,{get(){return t(),e[i]},enumerable:!0,configurable:!0});return a}function Bt(...e){let t=5381;for(const n of e)if(typeof n=="string"){let r=n.length;for(;r;)t=t*33^n.charCodeAt(--r)}else if(ArrayBuffer.isView(n)){const r=new Uint8Array(n.buffer,n.byteOffset,n.byteLength);let a=r.length;for(;a;)t=t*33^r[--a]}else throw new TypeError("value must be a string or TypedArray");return(t>>>0).toString(36)}new TextEncoder;const Mt=new TextDecoder;function Vt(e){const t=atob(e),n=new Uint8Array(t.length);for(let r=0;r<t.length;r++)n[r]=t.charCodeAt(r);return n}const qt=window.fetch;window.fetch=(e,t)=>((e instanceof Request?e.method:(t==null?void 0:t.method)||"GET")!=="GET"&&G.delete(Ie(e)),qt(e,t));const G=new Map;function zt(e,t){const n=Ie(e,t),r=document.querySelector(n);if(r!=null&&r.textContent){r.remove();let{body:a,...s}=JSON.parse(r.textContent);const i=r.getAttribute("data-ttl");return i&&G.set(n,{body:a,init:s,ttl:1e3*Number(i)}),r.getAttribute("data-b64")!==null&&(a=Vt(a)),Promise.resolve(new Response(a,s))}return window.fetch(e,t)}function Gt(e,t,n){if(G.size>0){const r=Ie(e,n),a=G.get(r);if(a){if(performance.now()<a.ttl&&["default","force-cache","only-if-cached",void 0].includes(n==null?void 0:n.cache))return new Response(a.body,a.init);G.delete(r)}}return window.fetch(t,n)}function Ie(e,t){let r=`script[data-sveltekit-fetched][data-url=${JSON.stringify(e instanceof Request?e.url:e)}]`;if(t!=null&&t.headers||t!=null&&t.body){const a=[];t.headers&&a.push([...new Headers(t.headers)].join(",")),t.body&&(typeof t.body=="string"||ArrayBuffer.isView(t.body))&&a.push(t.body),r+=`[data-hash="${Bt(...a)}"]`}return r}const Yt=/^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;function Ht(e){const t=[];return{pattern:e==="/"?/^\/$/:new RegExp(`^${Wt(e).map(r=>{const a=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(r);if(a)return t.push({name:a[1],matcher:a[2],optional:!1,rest:!0,chained:!0}),"(?:/([^]*))?";const s=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(r);if(s)return t.push({name:s[1],matcher:s[2],optional:!0,rest:!1,chained:!0}),"(?:/([^/]+))?";if(!r)return;const i=r.split(/\[(.+?)\](?!\])/);return"/"+i.map((c,l)=>{if(l%2){if(c.startsWith("x+"))return _e(String.fromCharCode(parseInt(c.slice(2),16)));if(c.startsWith("u+"))return _e(String.fromCharCode(...c.slice(2).split("-").map(u=>parseInt(u,16))));const d=Yt.exec(c),[,h,y,f,p]=d;return t.push({name:f,matcher:p,optional:!!h,rest:!!y,chained:y?l===1&&i[0]==="":!1}),y?"([^]*?)":h?"([^/]*)?":"([^/]+?)"}return _e(c)}).join("")}).join("")}/?$`),params:t}}function Kt(e){return e!==""&&!/^\([^)]+\)$/.test(e)}function Wt(e){return e.slice(1).split("/").filter(Kt)}function Jt(e,t,n){const r={},a=e.slice(1),s=a.filter(o=>o!==void 0);let i=0;for(let o=0;o<t.length;o+=1){const c=t[o];let l=a[o-i];if(c.chained&&c.rest&&i&&(l=a.slice(o-i,o+1).filter(d=>d).join("/"),i=0),l===void 0){c.rest&&(r[c.name]="");continue}if(!c.matcher||n[c.matcher](l)){r[c.name]=l;const d=t[o+1],h=a[o+1];d&&!d.rest&&d.optional&&h&&c.chained&&(i=0),!d&&!h&&Object.keys(r).length===s.length&&(i=0);continue}if(c.optional&&c.chained){i++;continue}return}if(!i)return r}function _e(e){return e.normalize().replace(/[[\]]/g,"\\$&").replace(/%/g,"%25").replace(/\//g,"%2[Ff]").replace(/\?/g,"%3[Ff]").replace(/#/g,"%23").replace(/[.*+?^${}()|\\]/g,"\\$&")}function Xt({nodes:e,server_loads:t,dictionary:n,matchers:r}){const a=new Set(t);return Object.entries(n).map(([o,[c,l,d]])=>{const{pattern:h,params:y}=Ht(o),f={id:o,exec:p=>{const u=h.exec(p);if(u)return Jt(u,y,r)},errors:[1,...d||[]].map(p=>e[p]),layouts:[0,...l||[]].map(i),leaf:s(c)};return f.errors.length=f.layouts.length=Math.max(f.errors.length,f.layouts.length),f});function s(o){const c=o<0;return c&&(o=~o),[c,e[o]]}function i(o){return o===void 0?o:[a.has(o),e[o]]}}function Qe(e,t=JSON.parse){try{return t(sessionStorage[e])}catch{}}function Be(e,t,n=JSON.stringify){const r=n(t);try{sessionStorage[e]=r}catch{}}const D=[];function Ue(e,t=ge){let n;const r=new Set;function a(o){if(Ct(e,o)&&(e=o,n)){const c=!D.length;for(const l of r)l[1](),D.push(l,e);if(c){for(let l=0;l<D.length;l+=2)D[l][0](D[l+1]);D.length=0}}}function s(o){a(o(e))}function i(o,c=ge){const l=[o,c];return r.add(l),r.size===1&&(n=t(a,s)||ge),o(e),()=>{r.delete(l),r.size===0&&n&&(n(),n=null)}}return{set:a,update:s,subscribe:i}}var Je;const $=((Je=globalThis.__sveltekit_rw16zh)==null?void 0:Je.base)??"";var Xe;const Zt=((Xe=globalThis.__sveltekit_rw16zh)==null?void 0:Xe.assets)??$??"",Qt="1761004654684",et="sveltekit:snapshot",tt="sveltekit:scroll",nt="sveltekit:states",en="sveltekit:pageurl",B="sveltekit:history",K="sveltekit:navigation",O={tap:1,hover:2,viewport:3,eager:4,off:-1,false:-1},ce=location.origin;function at(e){if(e instanceof URL)return e;let t=document.baseURI;if(!t){const n=document.getElementsByTagName("base");t=n.length?n[0].href:document.URL}return new URL(e,t)}function le(){return{x:pageXOffset,y:pageYOffset}}function F(e,t){return e.getAttribute(`data-sveltekit-${t}`)}const Me={...O,"":O.hover};function rt(e){let t=e.assignedSlot??e.parentNode;return(t==null?void 0:t.nodeType)===11&&(t=t.host),t}function ot(e,t){for(;e&&e!==t;){if(e.nodeName.toUpperCase()==="A"&&e.hasAttribute("href"))return e;e=rt(e)}}function Se(e,t,n){let r;try{if(r=new URL(e instanceof SVGAElement?e.href.baseVal:e.href,document.baseURI),n&&r.hash.match(/^#[^/]/)){const o=location.hash.split("#")[1]||"/";r.hash=`#${o}${r.hash}`}}catch{}const a=e instanceof SVGAElement?e.target.baseVal:e.target,s=!r||!!a||fe(r,t,n)||(e.getAttribute("rel")||"").split(/\s+/).includes("external"),i=(r==null?void 0:r.origin)===ce&&e.hasAttribute("download");return{url:r,external:s,target:a,download:i}}function Q(e){let t=null,n=null,r=null,a=null,s=null,i=null,o=e;for(;o&&o!==document.documentElement;)r===null&&(r=F(o,"preload-code")),a===null&&(a=F(o,"preload-data")),t===null&&(t=F(o,"keepfocus")),n===null&&(n=F(o,"noscroll")),s===null&&(s=F(o,"reload")),i===null&&(i=F(o,"replacestate")),o=rt(o);function c(l){switch(l){case"":case"true":return!0;case"off":case"false":return!1;default:return}}return{preload_code:Me[r??"off"],preload_data:Me[a??"off"],keepfocus:c(t),noscroll:c(n),reload:c(s),replace_state:c(i)}}function Ve(e){const t=Ue(e);let n=!0;function r(){n=!0,t.update(i=>i)}function a(i){n=!1,t.set(i)}function s(i){let o;return t.subscribe(c=>{(o===void 0||n&&c!==o)&&i(o=c)})}return{notify:r,set:a,subscribe:s}}const st={v:()=>{}};function tn(){const{set:e,subscribe:t}=Ue(!1);let n;async function r(){clearTimeout(n);try{const a=await fetch(`${Zt}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(!a.ok)return!1;const i=(await a.json()).version!==Qt;return i&&(e(!0),st.v(),clearTimeout(n)),i}catch{return!1}}return{subscribe:t,check:r}}function fe(e,t,n){return e.origin!==ce||!e.pathname.startsWith(t)?!0:n?!(e.pathname===t+"/"||e.pathname===t+"/index.html"||e.protocol==="file:"&&e.pathname.replace(/\/[^/]+\.html?$/,"")===t):!1}function qn(e){}function nn(e){const t=rn(e),n=new ArrayBuffer(t.length),r=new DataView(n);for(let a=0;a<n.byteLength;a++)r.setUint8(a,t.charCodeAt(a));return n}const an="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";function rn(e){e.length%4===0&&(e=e.replace(/==?$/,""));let t="",n=0,r=0;for(let a=0;a<e.length;a++)n<<=6,n|=an.indexOf(e[a]),r+=6,r===24&&(t+=String.fromCharCode((n&16711680)>>16),t+=String.fromCharCode((n&65280)>>8),t+=String.fromCharCode(n&255),n=r=0);return r===12?(n>>=4,t+=String.fromCharCode(n)):r===18&&(n>>=2,t+=String.fromCharCode((n&65280)>>8),t+=String.fromCharCode(n&255)),t}const on=-1,sn=-2,cn=-3,ln=-4,fn=-5,un=-6;function dn(e,t){if(typeof e=="number")return a(e,!0);if(!Array.isArray(e)||e.length===0)throw new Error("Invalid input");const n=e,r=Array(n.length);function a(s,i=!1){if(s===on)return;if(s===cn)return NaN;if(s===ln)return 1/0;if(s===fn)return-1/0;if(s===un)return-0;if(i||typeof s!="number")throw new Error("Invalid input");if(s in r)return r[s];const o=n[s];if(!o||typeof o!="object")r[s]=o;else if(Array.isArray(o))if(typeof o[0]=="string"){const c=o[0],l=t==null?void 0:t[c];if(l)return r[s]=l(a(o[1]));switch(c){case"Date":r[s]=new Date(o[1]);break;case"Set":const d=new Set;r[s]=d;for(let f=1;f<o.length;f+=1)d.add(a(o[f]));break;case"Map":const h=new Map;r[s]=h;for(let f=1;f<o.length;f+=2)h.set(a(o[f]),a(o[f+1]));break;case"RegExp":r[s]=new RegExp(o[1],o[2]);break;case"Object":r[s]=Object(o[1]);break;case"BigInt":r[s]=BigInt(o[1]);break;case"null":const y=Object.create(null);r[s]=y;for(let f=1;f<o.length;f+=2)y[o[f]]=a(o[f+1]);break;case"Int8Array":case"Uint8Array":case"Uint8ClampedArray":case"Int16Array":case"Uint16Array":case"Int32Array":case"Uint32Array":case"Float32Array":case"Float64Array":case"BigInt64Array":case"BigUint64Array":{const f=globalThis[c],p=new f(a(o[1]));r[s]=o[2]!==void 0?p.subarray(o[2],o[3]):p;break}case"ArrayBuffer":{const f=o[1],p=nn(f);r[s]=p;break}case"Temporal.Duration":case"Temporal.Instant":case"Temporal.PlainDate":case"Temporal.PlainTime":case"Temporal.PlainDateTime":case"Temporal.PlainMonthDay":case"Temporal.PlainYearMonth":case"Temporal.ZonedDateTime":{const f=c.slice(9);r[s]=Temporal[f].from(o[1]);break}case"URL":{const f=new URL(o[1]);r[s]=f;break}case"URLSearchParams":{const f=new URLSearchParams(o[1]);r[s]=f;break}default:throw new Error(`Unknown type ${c}`)}}else{const c=new Array(o.length);r[s]=c;for(let l=0;l<o.length;l+=1){const d=o[l];d!==sn&&(c[l]=a(d))}}else{const c={};r[s]=c;for(const l in o){if(l==="__proto__")throw new Error("Cannot parse an object with a `__proto__` property");const d=o[l];c[l]=a(d)}}return r[s]}return a(0)}const it=new Set(["load","prerender","csr","ssr","trailingSlash","config"]);[...it];const hn=new Set([...it]);[...hn];function pn(e){return e.filter(t=>t!=null)}const gn="x-sveltekit-invalidated",mn="x-sveltekit-trailing-slash";function ee(e){return e instanceof ie||e instanceof Te?e.status:500}function _n(e){return e instanceof Te?e.text:"Internal Error"}let T,W,we;const wn=ve.toString().includes("$$")||/function \w+\(\) \{\}/.test(ve.toString());wn?(T={data:{},form:null,error:null,params:{},route:{id:null},state:{},status:-1,url:new URL("https://example.com")},W={current:null},we={current:!1}):(T=new class{constructor(){R(this,"data",$state.raw({}));R(this,"form",$state.raw(null));R(this,"error",$state.raw(null));R(this,"params",$state.raw({}));R(this,"route",$state.raw({id:null}));R(this,"state",$state.raw({}));R(this,"status",$state.raw(-1));R(this,"url",$state.raw(new URL("https://example.com")))}},W=new class{constructor(){R(this,"current",$state.raw(null))}},we=new class{constructor(){R(this,"current",$state.raw(!1))}},st.v=()=>we.current=!0);function yn(e){Object.assign(T,e)}const vn="/__data.json",bn=".html__data.json";function Sn(e){return e.endsWith(".html")?e.replace(/\.html$/,bn):e.replace(/\/$/,"")+vn}const qe={spanContext(){return kn},setAttribute(){return this},setAttributes(){return this},addEvent(){return this},setStatus(){return this},updateName(){return this},end(){return this},isRecording(){return!1},recordException(){return this},addLink(){return this},addLinks(){return this}},kn={traceId:"",spanId:"",traceFlags:0},{tick:En}=Ot,An=new Set(["icon","shortcut icon","apple-touch-icon"]),j=Qe(tt)??{},J=Qe(et)??{},C={url:Ve({}),page:Ve({}),navigating:Ue(null),updated:tn()};function Le(e){j[e]=le()}function Rn(e,t){let n=e+1;for(;j[n];)delete j[n],n+=1;for(n=t+1;J[n];)delete J[n],n+=1}function q(e,t=!1){return t?location.replace(e.href):location.href=e.href,new Promise(()=>{})}async function ct(){if("serviceWorker"in navigator){const e=await navigator.serviceWorker.getRegistration($||"/");e&&await e.update()}}function ze(){}let $e,ke,te,x,Ee,k;const ne=[],ae=[];let U=null;const Z=new Map,lt=new Set,Tn=new Set,Y=new Set;let b={branch:[],error:null,url:null},xe=!1,re=!1,Ge=!0,X=!1,z=!1,ft=!1,Ce=!1,ut,A,L,N;const H=new Set,Ye=new Map;async function Hn(e,t,n){var s,i,o,c,l;(s=globalThis.__sveltekit_rw16zh)!=null&&s.data&&globalThis.__sveltekit_rw16zh.data,document.URL!==location.href&&(location.href=location.href),k=e,await((o=(i=e.hooks).init)==null?void 0:o.call(i)),$e=Xt(e),x=document.documentElement,Ee=t,ke=e.nodes[0],te=e.nodes[1],ke(),te(),A=(c=history.state)==null?void 0:c[B],L=(l=history.state)==null?void 0:l[K],A||(A=L=Date.now(),history.replaceState({...history.state,[B]:A,[K]:L},""));const r=j[A];function a(){r&&(history.scrollRestoration="manual",scrollTo(r.x,r.y))}n?(a(),await Dn(Ee,n)):(await M({type:"enter",url:at(k.hash?Bn(new URL(location.href)):location.href),replace_state:!0}),a()),jn()}function In(){ne.length=0,Ce=!1}function dt(e){ae.some(t=>t==null?void 0:t.snapshot)&&(J[e]=ae.map(t=>{var n;return(n=t==null?void 0:t.snapshot)==null?void 0:n.capture()}))}function ht(e){var t;(t=J[e])==null||t.forEach((n,r)=>{var a,s;(s=(a=ae[r])==null?void 0:a.snapshot)==null||s.restore(n)})}function He(){Le(A),Be(tt,j),dt(L),Be(et,J)}async function Un(e,t,n,r){let a;t.invalidateAll&&(U=null),await M({type:"goto",url:at(e),keepfocus:t.keepFocus,noscroll:t.noScroll,replace_state:t.replaceState,state:t.state,redirect_count:n,nav_token:r,accept:()=>{t.invalidateAll&&(Ce=!0,a=[...Ye.keys()]),t.invalidate&&t.invalidate.forEach(Nn)}}),t.invalidateAll&&be().then(be).then(()=>{Ye.forEach(({resource:s},i)=>{var o;a!=null&&a.includes(i)&&((o=s.refresh)==null||o.call(s))})})}async function Ln(e){if(e.id!==(U==null?void 0:U.id)){const t={};H.add(t),U={id:e.id,token:t,promise:mt({...e,preload:t}).then(n=>(H.delete(t),n.type==="loaded"&&n.state.error&&(U=null),n))}}return U.promise}async function ye(e){var n;const t=(n=await de(e,!1))==null?void 0:n.route;t&&await Promise.all([...t.layouts,t.leaf].map(r=>r==null?void 0:r[1]()))}function pt(e,t,n){var a;b=e.state;const r=document.querySelector("style[data-sveltekit]");if(r&&r.remove(),Object.assign(T,e.props.page),ut=new k.root({target:t,props:{...e.props,stores:C,components:ae},hydrate:n,sync:!1}),ht(L),n){const s={from:null,to:{params:b.params,route:{id:((a=b.route)==null?void 0:a.id)??null},url:new URL(location.href)},willUnload:!1,type:"enter",complete:Promise.resolve()};Y.forEach(i=>i(s))}re=!0}function oe({url:e,params:t,branch:n,status:r,error:a,route:s,form:i}){let o="never";if($&&(e.pathname===$||e.pathname===$+"/"))o="always";else for(const f of n)(f==null?void 0:f.slash)!==void 0&&(o=f.slash);e.pathname=Nt(e.pathname,o),e.search=e.search;const c={type:"loaded",state:{url:e,params:t,branch:n,error:a,route:s},props:{constructors:pn(n).map(f=>f.node.component),page:je(T)}};i!==void 0&&(c.props.form=i);let l={},d=!T,h=0;for(let f=0;f<Math.max(n.length,b.branch.length);f+=1){const p=n[f],u=b.branch[f];(p==null?void 0:p.data)!==(u==null?void 0:u.data)&&(d=!0),p&&(l={...l,...p.data},d&&(c.props[`data_${h}`]=l),h+=1)}return(!b.url||e.href!==b.url.href||b.error!==a||i!==void 0&&i!==T.form||d)&&(c.props.page={error:a,params:t,route:{id:(s==null?void 0:s.id)??null},state:{},status:r,url:new URL(e),form:i??null,data:d?l:T.data}),c}async function Pe({loader:e,parent:t,url:n,params:r,route:a,server_data_node:s}){var d,h,y;let i=null,o=!0;const c={dependencies:new Set,params:new Set,parent:!1,route:!1,url:!1,search_params:new Set},l=await e();if((d=l.universal)!=null&&d.load){let f=function(...u){for(const g of u){const{href:_}=new URL(g,n);c.dependencies.add(_)}};const p={tracing:{enabled:!1,root:qe,current:qe},route:new Proxy(a,{get:(u,g)=>(o&&(c.route=!0),u[g])}),params:new Proxy(r,{get:(u,g)=>(o&&c.params.add(g),u[g])}),data:(s==null?void 0:s.data)??null,url:Ft(n,()=>{o&&(c.url=!0)},u=>{o&&c.search_params.add(u)},k.hash),async fetch(u,g){u instanceof Request&&(g={body:u.method==="GET"||u.method==="HEAD"?void 0:await u.blob(),cache:u.cache,credentials:u.credentials,headers:[...u.headers].length>0?u==null?void 0:u.headers:void 0,integrity:u.integrity,keepalive:u.keepalive,method:u.method,mode:u.mode,redirect:u.redirect,referrer:u.referrer,referrerPolicy:u.referrerPolicy,signal:u.signal,...g});const{resolved:_,promise:I}=gt(u,g,n);return o&&f(_.href),I},setHeaders:()=>{},depends:f,parent(){return o&&(c.parent=!0),t()},untrack(u){o=!1;try{return u()}finally{o=!0}}};i=await l.universal.load.call(null,p)??null}return{node:l,loader:e,server:s,universal:(h=l.universal)!=null&&h.load?{type:"data",data:i,uses:c}:null,data:i??(s==null?void 0:s.data)??null,slash:((y=l.universal)==null?void 0:y.trailingSlash)??(s==null?void 0:s.slash)}}function gt(e,t,n){let r=e instanceof Request?e.url:e;const a=new URL(r,n);a.origin===n.origin&&(r=a.href.slice(n.origin.length));const s=re?Gt(r,a.href,t):zt(r,t);return{resolved:a,promise:s}}function Ke(e,t,n,r,a,s){if(Ce)return!0;if(!a)return!1;if(a.parent&&e||a.route&&t||a.url&&n)return!0;for(const i of a.search_params)if(r.has(i))return!0;for(const i of a.params)if(s[i]!==b.params[i])return!0;for(const i of a.dependencies)if(ne.some(o=>o(new URL(i))))return!0;return!1}function Oe(e,t){return(e==null?void 0:e.type)==="data"?e:(e==null?void 0:e.type)==="skip"?t??null:null}function $n(e,t){if(!e)return new Set(t.searchParams.keys());const n=new Set([...e.searchParams.keys(),...t.searchParams.keys()]);for(const r of n){const a=e.searchParams.getAll(r),s=t.searchParams.getAll(r);a.every(i=>s.includes(i))&&s.every(i=>a.includes(i))&&n.delete(r)}return n}function We({error:e,url:t,route:n,params:r}){return{type:"loaded",state:{error:e,url:t,route:n,params:r,branch:[]},props:{page:je(T),constructors:[]}}}async function mt({id:e,invalidating:t,url:n,params:r,route:a,preload:s}){if((U==null?void 0:U.id)===e)return H.delete(U.token),U.promise;const{errors:i,layouts:o,leaf:c}=a,l=[...o,c];i.forEach(w=>w==null?void 0:w().catch(()=>{})),l.forEach(w=>w==null?void 0:w[1]().catch(()=>{}));let d=null;const h=b.url?e!==se(b.url):!1,y=b.route?a.id!==b.route.id:!1,f=$n(b.url,n);let p=!1;const u=l.map((w,m)=>{var P;const v=b.branch[m],S=!!(w!=null&&w[0])&&((v==null?void 0:v.loader)!==w[1]||Ke(p,y,h,f,(P=v.server)==null?void 0:P.uses,r));return S&&(p=!0),S});if(u.some(Boolean)){try{d=await yt(n,u)}catch(w){const m=await V(w,{url:n,params:r,route:{id:e}});return H.has(s)?We({error:m,url:n,params:r,route:a}):ue({status:ee(w),error:m,url:n,route:a})}if(d.type==="redirect")return d}const g=d==null?void 0:d.nodes;let _=!1;const I=l.map(async(w,m)=>{var he;if(!w)return;const v=b.branch[m],S=g==null?void 0:g[m];if((!S||S.type==="skip")&&w[1]===(v==null?void 0:v.loader)&&!Ke(_,y,h,f,(he=v.universal)==null?void 0:he.uses,r))return v;if(_=!0,(S==null?void 0:S.type)==="error")throw S;return Pe({loader:w[1],url:n,params:r,route:a,parent:async()=>{var Fe;const De={};for(let pe=0;pe<m;pe+=1)Object.assign(De,(Fe=await I[pe])==null?void 0:Fe.data);return De},server_data_node:Oe(S===void 0&&w[0]?{type:"skip"}:S??null,w[0]?v==null?void 0:v.server:void 0)})});for(const w of I)w.catch(()=>{});const E=[];for(let w=0;w<l.length;w+=1)if(l[w])try{E.push(await I[w])}catch(m){if(m instanceof Re)return{type:"redirect",location:m.location};if(H.has(s))return We({error:await V(m,{params:r,url:n,route:{id:a.id}}),url:n,params:r,route:a});let v=ee(m),S;if(g!=null&&g.includes(m))v=m.status??v,S=m.error;else if(m instanceof ie)S=m.body;else{if(await C.updated.check())return await ct(),await q(n);S=await V(m,{params:r,url:n,route:{id:a.id}})}const P=await xn(w,E,i);return P?oe({url:n,params:r,branch:E.slice(0,P.idx).concat(P.node),status:v,error:S,route:a}):await wt(n,{id:a.id},S,v)}else E.push(void 0);return oe({url:n,params:r,branch:E,status:200,error:null,route:a,form:t?void 0:null})}async function xn(e,t,n){for(;e--;)if(n[e]){let r=e;for(;!t[r];)r-=1;try{return{idx:r+1,node:{node:await n[e](),loader:n[e],data:{},server:null,universal:null}}}catch{continue}}}async function ue({status:e,error:t,url:n,route:r}){const a={};let s=null;if(k.server_loads[0]===0)try{const o=await yt(n,[!0]);if(o.type!=="data"||o.nodes[0]&&o.nodes[0].type!=="data")throw 0;s=o.nodes[0]??null}catch{(n.origin!==ce||n.pathname!==location.pathname||xe)&&await q(n)}try{const o=await Pe({loader:ke,url:n,params:a,route:r,parent:()=>Promise.resolve({}),server_data_node:Oe(s)}),c={node:await te(),loader:te,universal:null,server:null,data:null};return oe({url:n,params:a,branch:[o,c],status:e,error:t,route:null})}catch(o){if(o instanceof Re)return Un(new URL(o.location,location.href),{},0);throw o}}async function Cn(e){const t=e.href;if(Z.has(t))return Z.get(t);let n;try{const r=(async()=>{let a=await k.hooks.reroute({url:new URL(e),fetch:async(s,i)=>gt(s,i,e).promise})??e;if(typeof a=="string"){const s=new URL(e);k.hash?s.hash=a:s.pathname=a,a=s}return a})();Z.set(t,r),n=await r}catch{Z.delete(t);return}return n}async function de(e,t){if(e&&!fe(e,$,k.hash)){const n=await Cn(e);if(!n)return;const r=Pn(n);for(const a of $e){const s=a.exec(r);if(s)return{id:se(e),invalidating:t,route:a,params:Dt(s),url:e}}}}function Pn(e){return jt(k.hash?e.hash.replace(/^#/,"").replace(/[?#].+/,""):e.pathname.slice($.length))||"/"}function se(e){return(k.hash?e.hash.replace(/^#/,""):e.pathname)+e.search}function _t({url:e,type:t,intent:n,delta:r,event:a}){let s=!1;const i=Ne(b,n,e,t);r!==void 0&&(i.navigation.delta=r),a!==void 0&&(i.navigation.event=a);const o={...i.navigation,cancel:()=>{s=!0,i.reject(new Error("navigation cancelled"))}};return X||lt.forEach(c=>c(o)),s?null:i}async function M({type:e,url:t,popped:n,keepfocus:r,noscroll:a,replace_state:s,state:i={},redirect_count:o=0,nav_token:c={},accept:l=ze,block:d=ze,event:h}){const y=N;N=c;const f=await de(t,!1),p=e==="enter"?Ne(b,f,t,e):_t({url:t,type:e,delta:n==null?void 0:n.delta,intent:f,event:h});if(!p){d(),N===c&&(N=y);return}const u=A,g=L;l(),X=!0,re&&p.navigation.type!=="enter"&&C.navigating.set(W.current=p.navigation);let _=f&&await mt(f);if(!_){if(fe(t,$,k.hash))return await q(t,s);_=await wt(t,{id:null},await V(new Te(404,"Not Found",`Not found: ${t.pathname}`),{url:t,params:{},route:{id:null}}),404,s)}if(t=(f==null?void 0:f.url)||t,N!==c)return p.reject(new Error("navigation aborted")),!1;if(_.type==="redirect"){if(o<20){await M({type:e,url:new URL(_.location,t),popped:n,keepfocus:r,noscroll:a,replace_state:s,state:i,redirect_count:o+1,nav_token:c}),p.fulfil(void 0);return}_=await ue({status:500,error:await V(new Error("Redirect loop"),{url:t,params:{},route:{id:null}}),url:t,route:{id:null}})}else _.props.page.status>=400&&await C.updated.check()&&(await ct(),await q(t,s));if(In(),Le(u),dt(g),_.props.page.url.pathname!==t.pathname&&(t.pathname=_.props.page.url.pathname),i=n?n.state:i,!n){const m=s?0:1,v={[B]:A+=m,[K]:L+=m,[nt]:i};(s?history.replaceState:history.pushState).call(history,v,"",t),s||Rn(A,L)}if(U=null,_.props.page.state=i,re){const m=(await Promise.all(Array.from(Tn,v=>v(p.navigation)))).filter(v=>typeof v=="function");if(m.length>0){let v=function(){m.forEach(S=>{Y.delete(S)})};m.push(v),m.forEach(S=>{Y.add(S)})}b=_.state,_.props.page&&(_.props.page.url=t),ut.$set(_.props),yn(_.props.page),ft=!0}else pt(_,Ee,!1);const{activeElement:I}=document;await En();let E=n?n.scroll:a?le():null;if(Ge){const m=t.hash&&document.getElementById(bt(t));if(E)scrollTo(E.x,E.y);else if(m){m.scrollIntoView();const{top:v,left:S}=m.getBoundingClientRect();E={x:pageXOffset+S,y:pageYOffset+v}}else scrollTo(0,0)}const w=document.activeElement!==I&&document.activeElement!==document.body;!r&&!w&&Fn(t,E),Ge=!0,_.props.page&&Object.assign(T,_.props.page),X=!1,e==="popstate"&&ht(L),p.fulfil(void 0),Y.forEach(m=>m(p.navigation)),C.navigating.set(W.current=null)}async function wt(e,t,n,r,a){return e.origin===ce&&e.pathname===location.pathname&&!xe?await ue({status:r,error:n,url:e,route:t}):await q(e,a)}function On(){let e,t,n;x.addEventListener("mousemove",o=>{const c=o.target;clearTimeout(e),e=setTimeout(()=>{s(c,O.hover)},20)});function r(o){o.defaultPrevented||s(o.composedPath()[0],O.tap)}x.addEventListener("mousedown",r),x.addEventListener("touchstart",r,{passive:!0});const a=new IntersectionObserver(o=>{for(const c of o)c.isIntersecting&&(ye(new URL(c.target.href)),a.unobserve(c.target))},{threshold:0});async function s(o,c){const l=ot(o,x),d=l===t&&c>=n;if(!l||d)return;const{url:h,external:y,download:f}=Se(l,$,k.hash);if(y||f)return;const p=Q(l),u=h&&se(b.url)===se(h);if(!(p.reload||u))if(c<=p.preload_data){t=l,n=O.tap;const g=await de(h,!1);if(!g)return;Ln(g)}else c<=p.preload_code&&(t=l,n=c,ye(h))}function i(){a.disconnect();for(const o of x.querySelectorAll("a")){const{url:c,external:l,download:d}=Se(o,$,k.hash);if(l||d)continue;const h=Q(o);h.reload||(h.preload_code===O.viewport&&a.observe(o),h.preload_code===O.eager&&ye(c))}}Y.add(i),i()}function V(e,t){if(e instanceof ie)return e.body;const n=ee(e),r=_n(e);return k.hooks.handleError({error:e,event:t,status:n,message:r})??{message:r}}function Nn(e){if(typeof e=="function")ne.push(e);else{const{href:t}=new URL(e,location.href);ne.push(n=>n.href===t)}}function jn(){var t;history.scrollRestoration="manual",addEventListener("beforeunload",n=>{let r=!1;if(He(),!X){const a=Ne(b,void 0,null,"leave"),s={...a.navigation,cancel:()=>{r=!0,a.reject(new Error("navigation cancelled"))}};lt.forEach(i=>i(s))}r?(n.preventDefault(),n.returnValue=""):history.scrollRestoration="auto"}),addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&He()}),(t=navigator.connection)!=null&&t.saveData||On(),x.addEventListener("click",async n=>{if(n.button||n.which!==1||n.metaKey||n.ctrlKey||n.shiftKey||n.altKey||n.defaultPrevented)return;const r=ot(n.composedPath()[0],x);if(!r)return;const{url:a,external:s,target:i,download:o}=Se(r,$,k.hash);if(!a)return;if(i==="_parent"||i==="_top"){if(window.parent!==window)return}else if(i&&i!=="_self")return;const c=Q(r);if(!(r instanceof SVGAElement)&&a.protocol!==location.protocol&&!(a.protocol==="https:"||a.protocol==="http:")||o)return;const[d,h]=(k.hash?a.hash.replace(/^#/,""):a.href).split("#"),y=d===me(location);if(s||c.reload&&(!y||!h)){_t({url:a,type:"link",event:n})?X=!0:n.preventDefault();return}if(h!==void 0&&y){const[,f]=b.url.href.split("#");if(f===h){if(n.preventDefault(),h===""||h==="top"&&r.ownerDocument.getElementById("top")===null)scrollTo({top:0});else{const p=r.ownerDocument.getElementById(decodeURIComponent(h));p&&(p.scrollIntoView(),p.focus())}return}if(z=!0,Le(A),e(a),!c.replace_state)return;z=!1}n.preventDefault(),await new Promise(f=>{requestAnimationFrame(()=>{setTimeout(f,0)}),setTimeout(f,100)}),await M({type:"link",url:a,keepfocus:c.keepfocus,noscroll:c.noscroll,replace_state:c.replace_state??a.href===location.href,event:n})}),x.addEventListener("submit",n=>{if(n.defaultPrevented)return;const r=HTMLFormElement.prototype.cloneNode.call(n.target),a=n.submitter;if(((a==null?void 0:a.formTarget)||r.target)==="_blank"||((a==null?void 0:a.formMethod)||r.method)!=="get")return;const o=new URL((a==null?void 0:a.hasAttribute("formaction"))&&(a==null?void 0:a.formAction)||r.action);if(fe(o,$,!1))return;const c=n.target,l=Q(c);if(l.reload)return;n.preventDefault(),n.stopPropagation();const d=new FormData(c,a);o.search=new URLSearchParams(d).toString(),M({type:"form",url:o,keepfocus:l.keepfocus,noscroll:l.noscroll,replace_state:l.replace_state??o.href===location.href,event:n})}),addEventListener("popstate",async n=>{var r;if(!Ae){if((r=n.state)!=null&&r[B]){const a=n.state[B];if(N={},a===A)return;const s=j[a],i=n.state[nt]??{},o=new URL(n.state[en]??location.href),c=n.state[K],l=b.url?me(location)===me(b.url):!1;if(c===L&&(ft||l)){i!==T.state&&(T.state=i),e(o),j[A]=le(),s&&scrollTo(s.x,s.y),A=a;return}const h=a-A;await M({type:"popstate",url:o,popped:{state:i,scroll:s,delta:h},accept:()=>{A=a,L=c},block:()=>{history.go(-h)},nav_token:N,event:n})}else if(!z){const a=new URL(location.href);e(a),k.hash&&location.reload()}}}),addEventListener("hashchange",()=>{z&&(z=!1,history.replaceState({...history.state,[B]:++A,[K]:L},"",location.href))});for(const n of document.querySelectorAll("link"))An.has(n.rel)&&(n.href=n.href);addEventListener("pageshow",n=>{n.persisted&&C.navigating.set(W.current=null)});function e(n){b.url=T.url=n,C.page.set(je(T)),C.page.notify()}}async function Dn(e,{status:t=200,error:n,node_ids:r,params:a,route:s,server_route:i,data:o,form:c}){xe=!0;const l=new URL(location.href);let d;({params:a={},route:s={id:null}}=await de(l,!1)||{}),d=$e.find(({id:f})=>f===s.id);let h,y=!0;try{const f=r.map(async(u,g)=>{const _=o[g];return _!=null&&_.uses&&(_.uses=vt(_.uses)),Pe({loader:k.nodes[u],url:l,params:a,route:s,parent:async()=>{const I={};for(let E=0;E<g;E+=1)Object.assign(I,(await f[E]).data);return I},server_data_node:Oe(_)})}),p=await Promise.all(f);if(d){const u=d.layouts;for(let g=0;g<u.length;g++)u[g]||p.splice(g,0,void 0)}h=oe({url:l,params:a,branch:p,status:t,error:n,form:c,route:d??null})}catch(f){if(f instanceof Re){await q(new URL(f.location,location.href));return}h=await ue({status:ee(f),error:await V(f,{url:l,params:a,route:s}),url:l,route:s}),e.textContent="",y=!1}h.props.page&&(h.props.page.state={}),pt(h,e,y)}async function yt(e,t){var s;const n=new URL(e);n.pathname=Sn(e.pathname),e.pathname.endsWith("/")&&n.searchParams.append(mn,"1"),n.searchParams.append(gn,t.map(i=>i?"1":"0").join(""));const r=window.fetch,a=await r(n.href,{});if(!a.ok){let i;throw(s=a.headers.get("content-type"))!=null&&s.includes("application/json")?i=await a.json():a.status===404?i="Not Found":a.status===500&&(i="Internal Error"),new ie(a.status,i)}return new Promise(async i=>{var h;const o=new Map,c=a.body.getReader();function l(y){return dn(y,{...k.decoders,Promise:f=>new Promise((p,u)=>{o.set(f,{fulfil:p,reject:u})})})}let d="";for(;;){const{done:y,value:f}=await c.read();if(y&&!d)break;for(d+=!f&&d?`
|
| 2 |
+
`:Mt.decode(f,{stream:!0});;){const p=d.indexOf(`
|
| 3 |
+
`);if(p===-1)break;const u=JSON.parse(d.slice(0,p));if(d=d.slice(p+1),u.type==="redirect")return i(u);if(u.type==="data")(h=u.nodes)==null||h.forEach(g=>{(g==null?void 0:g.type)==="data"&&(g.uses=vt(g.uses),g.data=l(g.data))}),i(u);else if(u.type==="chunk"){const{id:g,data:_,error:I}=u,E=o.get(g);o.delete(g),I?E.reject(l(I)):E.fulfil(l(_))}}}})}function vt(e){return{dependencies:new Set((e==null?void 0:e.dependencies)??[]),params:new Set((e==null?void 0:e.params)??[]),parent:!!(e!=null&&e.parent),route:!!(e!=null&&e.route),url:!!(e!=null&&e.url),search_params:new Set((e==null?void 0:e.search_params)??[])}}let Ae=!1;function Fn(e,t=null){const n=document.querySelector("[autofocus]");if(n)n.focus();else{const r=bt(e);if(r&&document.getElementById(r)){const{x:s,y:i}=t??le();setTimeout(()=>{const o=history.state;Ae=!0,location.replace(`#${r}`),k.hash&&location.replace(e.hash),history.replaceState(o,"",e.hash),scrollTo(s,i),Ae=!1})}else{const s=document.body,i=s.getAttribute("tabindex");s.tabIndex=-1,s.focus({preventScroll:!0,focusVisible:!1}),i!==null?s.setAttribute("tabindex",i):s.removeAttribute("tabindex")}const a=getSelection();if(a&&a.type!=="None"){const s=[];for(let i=0;i<a.rangeCount;i+=1)s.push(a.getRangeAt(i));setTimeout(()=>{if(a.rangeCount===s.length){for(let i=0;i<a.rangeCount;i+=1){const o=s[i],c=a.getRangeAt(i);if(o.commonAncestorContainer!==c.commonAncestorContainer||o.startContainer!==c.startContainer||o.endContainer!==c.endContainer||o.startOffset!==c.startOffset||o.endOffset!==c.endOffset)return}a.removeAllRanges()}})}}}function Ne(e,t,n,r){var c,l;let a,s;const i=new Promise((d,h)=>{a=d,s=h});return i.catch(()=>{}),{navigation:{from:{params:e.params,route:{id:((c=e.route)==null?void 0:c.id)??null},url:e.url},to:n&&{params:(t==null?void 0:t.params)??null,route:{id:((l=t==null?void 0:t.route)==null?void 0:l.id)??null},url:n},willUnload:!t,type:r,complete:i},fulfil:a,reject:s}}function je(e){return{data:e.data,error:e.error,form:e.form,params:e.params,route:e.route,state:e.state,status:e.status,url:e.url}}function Bn(e){const t=new URL(e);return t.hash=decodeURIComponent(e.hash),t}function bt(e){let t;if(k.hash){const[,,n]=e.hash.split("#",3);t=n??""}else t=e.hash.slice(1);return decodeURIComponent(t)}export{Hn as a,qn as l,C as s};
|
hfstudio/static/_app/immutable/entry/app.CIwzfQ6B.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.UMzK7bBX.js","../chunks/BF3xBGh8.js","../chunks/IHki7fMi.js","../assets/0.DBGxRMBA.css","../nodes/1.i8TJb2hr.js","../chunks/DVK2ASb7.js","../nodes/2.1jor8E63.js","../assets/2.D7LovqyU.css"])))=>i.map(i=>d[i]);
|
| 2 |
+
import{S as V,i as j,s as B,d,t as h,a as g,C as S,D as O,f as v,o as U,E as w,q as z,F,v as G,G as H,H as y,I as P,J as R,K as L,L as I,b as A,M as p,k as J,m as K,p as W,N as C,x as Q,y as X,z as Y}from"../chunks/BF3xBGh8.js";import"../chunks/IHki7fMi.js";const Z="modulepreload",M=function(o,e){return new URL(o,e).href},N={},D=function(e,n,i){let r=Promise.resolve();if(n&&n.length>0){const t=document.getElementsByTagName("link"),s=document.querySelector("meta[property=csp-nonce]"),a=(s==null?void 0:s.nonce)||(s==null?void 0:s.getAttribute("nonce"));r=Promise.allSettled(n.map(f=>{if(f=M(f,i),f in N)return;N[f]=!0;const l=f.endsWith(".css"),_=l?'[rel="stylesheet"]':"";if(!!i)for(let k=t.length-1;k>=0;k--){const E=t[k];if(E.href===f&&(!l||E.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${f}"]${_}`))return;const m=document.createElement("link");if(m.rel=l?"stylesheet":Z,l||(m.as="script"),m.crossOrigin="",m.href=f,a&&m.setAttribute("nonce",a),document.head.appendChild(m),l)return new Promise((k,E)=>{m.addEventListener("load",k),m.addEventListener("error",()=>E(new Error(`Unable to preload CSS for ${f}`)))})}))}function u(t){const s=new Event("vite:preloadError",{cancelable:!0});if(s.payload=t,window.dispatchEvent(s),!s.defaultPrevented)throw t}return r.then(t=>{for(const s of t||[])s.status==="rejected"&&u(s.reason);return e().catch(u)})},ae={};function $(o){let e,n,i;var r=o[2][0];function u(t,s){return{props:{data:t[4],form:t[3],params:t[1].params}}}return r&&(e=y(r,u(o)),o[12](e)),{c(){e&&R(e.$$.fragment),n=w()},l(t){e&&I(e.$$.fragment,t),n=w()},m(t,s){e&&L(e,t,s),v(t,n,s),i=!0},p(t,s){if(s&4&&r!==(r=t[2][0])){if(e){S();const a=e;h(a.$$.fragment,1,0,()=>{P(a,1)}),O()}r?(e=y(r,u(t)),t[12](e),R(e.$$.fragment),g(e.$$.fragment,1),L(e,n.parentNode,n)):e=null}else if(r){const a={};s&16&&(a.data=t[4]),s&8&&(a.form=t[3]),s&2&&(a.params=t[1].params),e.$set(a)}},i(t){i||(e&&g(e.$$.fragment,t),i=!0)},o(t){e&&h(e.$$.fragment,t),i=!1},d(t){t&&d(n),o[12](null),e&&P(e,t)}}}function x(o){let e,n,i;var r=o[2][0];function u(t,s){return{props:{data:t[4],params:t[1].params,$$slots:{default:[ee]},$$scope:{ctx:t}}}}return r&&(e=y(r,u(o)),o[11](e)),{c(){e&&R(e.$$.fragment),n=w()},l(t){e&&I(e.$$.fragment,t),n=w()},m(t,s){e&&L(e,t,s),v(t,n,s),i=!0},p(t,s){if(s&4&&r!==(r=t[2][0])){if(e){S();const a=e;h(a.$$.fragment,1,0,()=>{P(a,1)}),O()}r?(e=y(r,u(t)),t[11](e),R(e.$$.fragment),g(e.$$.fragment,1),L(e,n.parentNode,n)):e=null}else if(r){const a={};s&16&&(a.data=t[4]),s&2&&(a.params=t[1].params),s&8239&&(a.$$scope={dirty:s,ctx:t}),e.$set(a)}},i(t){i||(e&&g(e.$$.fragment,t),i=!0)},o(t){e&&h(e.$$.fragment,t),i=!1},d(t){t&&d(n),o[11](null),e&&P(e,t)}}}function ee(o){let e,n,i;var r=o[2][1];function u(t,s){return{props:{data:t[5],form:t[3],params:t[1].params}}}return r&&(e=y(r,u(o)),o[10](e)),{c(){e&&R(e.$$.fragment),n=w()},l(t){e&&I(e.$$.fragment,t),n=w()},m(t,s){e&&L(e,t,s),v(t,n,s),i=!0},p(t,s){if(s&4&&r!==(r=t[2][1])){if(e){S();const a=e;h(a.$$.fragment,1,0,()=>{P(a,1)}),O()}r?(e=y(r,u(t)),t[10](e),R(e.$$.fragment),g(e.$$.fragment,1),L(e,n.parentNode,n)):e=null}else if(r){const a={};s&32&&(a.data=t[5]),s&8&&(a.form=t[3]),s&2&&(a.params=t[1].params),e.$set(a)}},i(t){i||(e&&g(e.$$.fragment,t),i=!0)},o(t){e&&h(e.$$.fragment,t),i=!1},d(t){t&&d(n),o[10](null),e&&P(e,t)}}}function q(o){let e,n=o[7]&&T(o);return{c(){e=W("div"),n&&n.c(),this.h()},l(i){e=J(i,"DIV",{id:!0,"aria-live":!0,"aria-atomic":!0,style:!0});var r=K(e);n&&n.l(r),r.forEach(d),this.h()},h(){A(e,"id","svelte-announcer"),A(e,"aria-live","assertive"),A(e,"aria-atomic","true"),p(e,"position","absolute"),p(e,"left","0"),p(e,"top","0"),p(e,"clip","rect(0 0 0 0)"),p(e,"clip-path","inset(50%)"),p(e,"overflow","hidden"),p(e,"white-space","nowrap"),p(e,"width","1px"),p(e,"height","1px")},m(i,r){v(i,e,r),n&&n.m(e,null)},p(i,r){i[7]?n?n.p(i,r):(n=T(i),n.c(),n.m(e,null)):n&&(n.d(1),n=null)},d(i){i&&d(e),n&&n.d()}}}function T(o){let e;return{c(){e=Y(o[8])},l(n){e=X(n,o[8])},m(n,i){v(n,e,i)},p(n,i){i&256&&Q(e,n[8])},d(n){n&&d(e)}}}function te(o){let e,n,i,r,u;const t=[x,$],s=[];function a(l,_){return l[2][1]?0:1}e=a(o),n=s[e]=t[e](o);let f=o[6]&&q(o);return{c(){n.c(),i=z(),f&&f.c(),r=w()},l(l){n.l(l),i=U(l),f&&f.l(l),r=w()},m(l,_){s[e].m(l,_),v(l,i,_),f&&f.m(l,_),v(l,r,_),u=!0},p(l,[_]){let b=e;e=a(l),e===b?s[e].p(l,_):(S(),h(s[b],1,1,()=>{s[b]=null}),O(),n=s[e],n?n.p(l,_):(n=s[e]=t[e](l),n.c()),g(n,1),n.m(i.parentNode,i)),l[6]?f?f.p(l,_):(f=q(l),f.c(),f.m(r.parentNode,r)):f&&(f.d(1),f=null)},i(l){u||(g(n),u=!0)},o(l){h(n),u=!1},d(l){l&&(d(i),d(r)),s[e].d(l),f&&f.d(l)}}}function ne(o,e,n){let{stores:i}=e,{page:r}=e,{constructors:u}=e,{components:t=[]}=e,{form:s}=e,{data_0:a=null}=e,{data_1:f=null}=e;F(i.page.notify);let l=!1,_=!1,b=null;G(()=>{const c=i.page.subscribe(()=>{l&&(n(7,_=!0),H().then(()=>{n(8,b=document.title||"untitled page")}))});return n(6,l=!0),c});function m(c){C[c?"unshift":"push"](()=>{t[1]=c,n(0,t)})}function k(c){C[c?"unshift":"push"](()=>{t[0]=c,n(0,t)})}function E(c){C[c?"unshift":"push"](()=>{t[0]=c,n(0,t)})}return o.$$set=c=>{"stores"in c&&n(9,i=c.stores),"page"in c&&n(1,r=c.page),"constructors"in c&&n(2,u=c.constructors),"components"in c&&n(0,t=c.components),"form"in c&&n(3,s=c.form),"data_0"in c&&n(4,a=c.data_0),"data_1"in c&&n(5,f=c.data_1)},o.$$.update=()=>{o.$$.dirty&514&&i.page.set(r)},[t,r,u,s,a,f,l,_,b,i,m,k,E]}class le extends V{constructor(e){super(),j(this,e,ne,te,B,{stores:9,page:1,constructors:2,components:0,form:3,data_0:4,data_1:5})}}const fe=[()=>D(()=>import("../nodes/0.UMzK7bBX.js"),__vite__mapDeps([0,1,2,3]),import.meta.url),()=>D(()=>import("../nodes/1.i8TJb2hr.js"),__vite__mapDeps([4,1,2,5]),import.meta.url),()=>D(()=>import("../nodes/2.1jor8E63.js"),__vite__mapDeps([6,1,2,7]),import.meta.url)],ce=[],ue={"/":[2]},se={handleError:({error:o})=>{console.error(o)},reroute:()=>{},transport:{}},ie=Object.fromEntries(Object.entries(se.transport).map(([o,e])=>[o,e.decode])),_e=!1,me=(o,e)=>ie[o](e);export{me as decode,ie as decoders,ue as dictionary,_e as hash,se as hooks,ae as matchers,fe as nodes,le as root,ce as server_loads};
|
hfstudio/static/_app/immutable/entry/start.jV5w9ZfR.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{l as o,a as r}from"../chunks/DVK2ASb7.js";export{o as load_css,r as start};
|
hfstudio/static/_app/immutable/nodes/0.UMzK7bBX.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import{S as Se,i as Ce,s as Le,c as He,d as x,r as Te,t as Ee,a as Me,b as r,u as Ne,g as Ue,e as Ae,f as $,h as s,l as le,j as Pe,k as u,m as A,n as H,o as w,p as f,q as y,v as De,w as xe,x as me,y as te,z as se,A as Be}from"../chunks/BF3xBGh8.js";import"../chunks/IHki7fMi.js";function Oe(c){let t,e="Sign In";return{c(){t=f("span"),t.textContent=e},l(n){t=u(n,"SPAN",{"data-svelte-h":!0}),H(t)!=="svelte-6n3gky"&&(t.textContent=e)},m(n,h){$(n,t,h)},p:Be,d(n){n&&x(t)}}}function Ve(c){let t,e,n,h;return{c(){t=f("span"),e=se("Sign out ("),n=se(c[2]),h=se(")")},l(k){t=u(k,"SPAN",{});var o=A(t);e=te(o,"Sign out ("),n=te(o,c[2]),h=te(o,")"),o.forEach(x)},m(k,o){$(k,t,o),s(t,e),s(t,n),s(t,h)},p(k,o){o&4&&me(n,k[2])},d(k){k&&x(t)}}}function je(c){let t,e,n=c[1]?"logged in":"not logged in",h,k;return{c(){t=f("span"),e=se("Checking... ("),h=se(n),k=se(")")},l(o){t=u(o,"SPAN",{});var m=A(t);e=te(m,"Checking... ("),h=te(m,n),k=te(m,")"),m.forEach(x)},m(o,m){$(o,t,m),s(t,e),s(t,h),s(t,k)},p(o,m){m&2&&n!==(n=o[1]?"logged in":"not logged in")&&me(h,n)},d(o){o&&x(t)}}}function ke(c){let t,e=`<div class="bg-blue-600 text-white text-sm rounded-lg p-3 shadow-lg relative"><div class="flex items-start gap-2"><svg class="w-4 h-4 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg> <div><p class="font-medium">Sign in required</p> <p class="text-blue-100 text-xs mt-1">You need to sign in to use HuggingFace Inference Providers for text-to-speech
|
| 2 |
+
generation.</p></div></div> <div class="absolute top-full left-1/2 transform -translate-x-1/2"><div class="w-0 h-0 border-l-4 border-r-4 border-t-4 border-l-transparent border-r-transparent border-t-blue-600"></div></div></div>`;return{c(){t=f("div"),t.innerHTML=e,this.h()},l(n){t=u(n,"DIV",{class:!0,"data-svelte-h":!0}),H(t)!=="svelte-4csry"&&(t.innerHTML=e),this.h()},h(){r(t,"class","absolute bottom-full left-0 right-0 mb-2 z-50")},m(n,h){$(n,t,h)},d(n){n&&x(t)}}}function we(c){let t,e,n,h="Sign In with HuggingFace Token",k,o,m,Q="<strong>Manual Token Entry:</strong> Please enter your HuggingFace token.",v,d,E=`1. Go to <a href="https://huggingface.co/settings/tokens" target="_blank" class="underline text-blue-600">HuggingFace Settings</a><br/>
|
| 3 |
+
2. Create a new token with "Inference API" permissions<br/>
|
| 4 |
+
3. Copy and paste it below`,Y,z,p,I,F="HuggingFace Token",J,b,q,W,T,P,K="Cancel",D,V,X="Sign In",l,_,i=c[7]&&ye(),a=c[6]&&Ie(c);return{c(){t=f("div"),e=f("div"),n=f("h2"),n.textContent=h,k=y(),o=f("div"),m=f("p"),m.innerHTML=Q,v=y(),d=f("p"),d.innerHTML=E,Y=y(),i&&i.c(),z=y(),p=f("div"),I=f("label"),I.textContent=F,J=y(),b=f("input"),q=y(),a&&a.c(),W=y(),T=f("div"),P=f("button"),P.textContent=K,D=y(),V=f("button"),V.textContent=X,this.h()},l(S){t=u(S,"DIV",{class:!0});var G=A(t);e=u(G,"DIV",{class:!0});var L=A(e);n=u(L,"H2",{class:!0,"data-svelte-h":!0}),H(n)!=="svelte-1t0ehet"&&(n.textContent=h),k=w(L),o=u(L,"DIV",{class:!0});var C=A(o);m=u(C,"P",{class:!0,"data-svelte-h":!0}),H(m)!=="svelte-344vn4"&&(m.innerHTML=Q),v=w(C),d=u(C,"P",{class:!0,"data-svelte-h":!0}),H(d)!=="svelte-orsfwv"&&(d.innerHTML=E),Y=w(C),i&&i.l(C),C.forEach(x),z=w(L),p=u(L,"DIV",{class:!0});var B=A(p);I=u(B,"LABEL",{for:!0,class:!0,"data-svelte-h":!0}),H(I)!=="svelte-vtbmxo"&&(I.textContent=F),J=w(B),b=u(B,"INPUT",{id:!0,type:!0,placeholder:!0,class:!0}),q=w(B),a&&a.l(B),B.forEach(x),W=w(L),T=u(L,"DIV",{class:!0});var ee=A(T);P=u(ee,"BUTTON",{class:!0,"data-svelte-h":!0}),H(P)!=="svelte-csk0rj"&&(P.textContent=K),D=w(ee),V=u(ee,"BUTTON",{class:!0,"data-svelte-h":!0}),H(V)!=="svelte-1nxas5u"&&(V.textContent=X),ee.forEach(x),L.forEach(x),G.forEach(x),this.h()},h(){r(n,"class","text-xl font-semibold mb-4"),r(m,"class","text-blue-800 mb-2"),r(d,"class","text-blue-700"),r(o,"class","mb-4 p-3 bg-blue-50 rounded-md text-sm"),r(I,"for","token"),r(I,"class","block text-sm font-medium text-gray-700 mb-2"),r(b,"id","token"),r(b,"type","password"),r(b,"placeholder","hf_..."),r(b,"class","w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"),r(p,"class","mb-4"),r(P,"class","px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors"),r(V,"class","px-4 py-2 bg-orange-500 text-white rounded-md hover:bg-orange-600 transition-colors"),r(T,"class","flex justify-end gap-3"),r(e,"class","bg-white rounded-lg p-6 max-w-md w-full mx-4 shadow-xl"),r(t,"class","fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50")},m(S,G){$(S,t,G),s(t,e),s(e,n),s(e,k),s(e,o),s(o,m),s(o,v),s(o,d),s(o,Y),i&&i.m(o,null),s(e,z),s(e,p),s(p,I),s(p,J),s(p,b),xe(b,c[5]),s(p,q),a&&a.m(p,null),s(e,W),s(e,T),s(T,P),s(T,D),s(T,V),l||(_=[le(b,"input",c[15]),le(b,"keydown",c[16]),le(P,"click",c[10]),le(V,"click",c[11])],l=!0)},p(S,G){S[7]?i||(i=ye(),i.c(),i.m(o,null)):i&&(i.d(1),i=null),G&32&&b.value!==S[5]&&xe(b,S[5]),S[6]?a?a.p(S,G):(a=Ie(S),a.c(),a.m(p,null)):a&&(a.d(1),a=null)},d(S){S&&x(t),i&&i.d(),a&&a.d(),l=!1,Te(_)}}}function ye(c){let t,e=`<strong>Tip:</strong> You can also run <code>huggingface-cli login</code> in your terminal
|
| 5 |
+
to automatically use your local token.`;return{c(){t=f("p"),t.innerHTML=e,this.h()},l(n){t=u(n,"P",{class:!0,"data-svelte-h":!0}),H(t)!=="svelte-xrut8w"&&(t.innerHTML=e),this.h()},h(){r(t,"class","text-blue-600 mt-2")},m(n,h){$(n,t,h)},d(n){n&&x(t)}}}function Ie(c){let t,e;return{c(){t=f("p"),e=se(c[6]),this.h()},l(n){t=u(n,"P",{class:!0});var h=A(t);e=te(h,c[6]),h.forEach(x),this.h()},h(){r(t,"class","text-red-600 text-sm mt-1")},m(n,h){$(n,t,h),s(t,e)},p(n,h){h&64&&me(e,n[6])},d(n){n&&x(t)}}}function ze(c){let t,e,n,h='<div class="flex items-center gap-3"><img src="/assets/hf-studio-logo.png" alt="HF Logo" class="w-8 h-8"/> <h1 class="text-xl font-semibold">HFStudio<sup class="text-xs text-gray-500 ml-1">BETA</sup></h1></div>',k,o,m,Q="Tasks",v,d,E,Y="ποΈ",z,p,I="Text to Speech",F,J,b,q="<span>π΅</span> <span>Voice Cloning</span>",W,T,P="<span>π§</span> <span>Speech to Text</span>",K,D,V="<span>πΌ</span> <span>Sound Effects</span>",X,l,_="<span>πΈ</span> <span>Music Generation</span>",i,a,S="<span>π</span> <span>Audio Enhancement</span>",G,L,C,B,ee,ue,fe,de,ne,he,Z,pe,_e;function be(g,N){return g[3]?je:g[1]?Ve:Oe}let ae=be(c),R=ae(c),U=c[8]&&!c[1]&&ke();const ge=c[13].default,j=He(ge,c,c[12],null);let M=c[4]&&we(c);return{c(){t=f("div"),e=f("aside"),n=f("div"),n.innerHTML=h,k=y(),o=f("nav"),m=f("div"),m.textContent=Q,v=y(),d=f("button"),E=f("span"),E.textContent=Y,z=y(),p=f("span"),p.textContent=I,J=y(),b=f("button"),b.innerHTML=q,W=y(),T=f("button"),T.innerHTML=P,K=y(),D=f("button"),D.innerHTML=V,X=y(),l=f("button"),l.innerHTML=_,i=y(),a=f("button"),a.innerHTML=S,G=y(),L=f("div"),C=f("button"),B=f("img"),ue=y(),R.c(),fe=y(),U&&U.c(),de=y(),ne=f("main"),j&&j.c(),he=y(),M&&M.c(),this.h()},l(g){t=u(g,"DIV",{class:!0});var N=A(t);e=u(N,"ASIDE",{class:!0});var oe=A(e);n=u(oe,"DIV",{class:!0,"data-svelte-h":!0}),H(n)!=="svelte-60or62"&&(n.innerHTML=h),k=w(oe),o=u(oe,"NAV",{class:!0});var O=A(o);m=u(O,"DIV",{class:!0,"data-svelte-h":!0}),H(m)!=="svelte-pii1fa"&&(m.textContent=Q),v=w(O),d=u(O,"BUTTON",{class:!0});var re=A(d);E=u(re,"SPAN",{"data-svelte-h":!0}),H(E)!=="svelte-1yx42xi"&&(E.textContent=Y),z=w(re),p=u(re,"SPAN",{"data-svelte-h":!0}),H(p)!=="svelte-2j89jk"&&(p.textContent=I),re.forEach(x),J=w(O),b=u(O,"BUTTON",{class:!0,"data-svelte-h":!0}),H(b)!=="svelte-10dl8nf"&&(b.innerHTML=q),W=w(O),T=u(O,"BUTTON",{class:!0,"data-svelte-h":!0}),H(T)!=="svelte-wf0x5d"&&(T.innerHTML=P),K=w(O),D=u(O,"BUTTON",{class:!0,"data-svelte-h":!0}),H(D)!=="svelte-x7bha3"&&(D.innerHTML=V),X=w(O),l=u(O,"BUTTON",{class:!0,"data-svelte-h":!0}),H(l)!=="svelte-1tyblmt"&&(l.innerHTML=_),i=w(O),a=u(O,"BUTTON",{class:!0,"data-svelte-h":!0}),H(a)!=="svelte-1emrjb3"&&(a.innerHTML=S),O.forEach(x),G=w(oe),L=u(oe,"DIV",{class:!0});var ce=A(L);C=u(ce,"BUTTON",{class:!0});var ie=A(C);B=u(ie,"IMG",{src:!0,alt:!0,class:!0}),ue=w(ie),R.l(ie),ie.forEach(x),fe=w(ce),U&&U.l(ce),ce.forEach(x),oe.forEach(x),de=w(N),ne=u(N,"MAIN",{class:!0});var ve=A(ne);j&&j.l(ve),ve.forEach(x),he=w(N),M&&M.l(N),N.forEach(x),this.h()},h(){r(n,"class","p-4 border-b border-gray-200"),r(m,"class","mt-2 mb-1 px-2 text-xs font-medium text-gray-500 uppercase"),r(d,"class",F="w-full flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-100 transition-colors text-left "+(c[0]==="tts"?"bg-gray-100":"")),r(b,"class","w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"),b.disabled=!0,r(T,"class","w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"),T.disabled=!0,r(D,"class","w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"),D.disabled=!0,r(l,"class","w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"),l.disabled=!0,r(a,"class","w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"),a.disabled=!0,r(o,"class","p-2 text-sm flex-1"),Pe(B.src,ee="/assets/hf-logo.png")||r(B,"src",ee),r(B,"alt","HF Logo"),r(B,"class","w-5 h-5"),C.disabled=c[3],r(C,"class","w-full px-6 py-3 bg-black text-white rounded-lg font-medium hover:bg-gray-800 transition-colors shadow-sm flex items-center justify-center gap-2 text-sm disabled:opacity-50 disabled:cursor-not-allowed"),r(L,"class","p-2 relative"),r(e,"class","w-56 border-r border-gray-200 bg-white flex-shrink-0 flex flex-col h-full "),r(ne,"class","flex-1 overflow-auto"),r(t,"class","flex h-screen bg-white")},m(g,N){$(g,t,N),s(t,e),s(e,n),s(e,k),s(e,o),s(o,m),s(o,v),s(o,d),s(d,E),s(d,z),s(d,p),s(o,J),s(o,b),s(o,W),s(o,T),s(o,K),s(o,D),s(o,X),s(o,l),s(o,i),s(o,a),s(e,G),s(e,L),s(L,C),s(C,B),s(C,ue),R.m(C,null),s(L,fe),U&&U.m(L,null),s(t,de),s(t,ne),j&&j.m(ne,null),s(t,he),M&&M.m(t,null),Z=!0,pe||(_e=[le(d,"click",c[14]),le(C,"click",c[9])],pe=!0)},p(g,[N]){(!Z||N&1&&F!==(F="w-full flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-100 transition-colors text-left "+(g[0]==="tts"?"bg-gray-100":"")))&&r(d,"class",F),ae===(ae=be(g))&&R?R.p(g,N):(R.d(1),R=ae(g),R&&(R.c(),R.m(C,null))),(!Z||N&8)&&(C.disabled=g[3]),g[8]&&!g[1]?U||(U=ke(),U.c(),U.m(L,null)):U&&(U.d(1),U=null),j&&j.p&&(!Z||N&4096)&&Ne(j,ge,g,g[12],Z?Ae(ge,g[12],N,null):Ue(g[12]),null),g[4]?M?M.p(g,N):(M=we(g),M.c(),M.m(t,null)):M&&(M.d(1),M=null)},i(g){Z||(Me(j,g),Z=!0)},o(g){Ee(j,g),Z=!1},d(g){g&&x(t),R.d(),U&&U.d(),j&&j.d(g),M&&M.d(),pe=!1,Te(_e)}}}function Fe(c,t,e){let{$$slots:n={},$$scope:h}=t,k="tts";const o=typeof window<"u"?localStorage.getItem("hf_access_token"):null,m=typeof window<"u"?localStorage.getItem("hf_cached_token"):null,Q=typeof window<"u"?localStorage.getItem("hf_user_info"):null;let v=!1,d="",E=!1,Y=!1;if(o&&o===m&&Q)try{const l=JSON.parse(Q);v=!0,d=l.username,Y=!0}catch{E=!0}else o&&(E=!0);let z=!1,p="",I="",F=!1,J=!1;De(()=>{window.addEventListener("show-signin-popover",()=>{e(8,J=!0),setTimeout(()=>{e(8,J=!1)},4e3)}),!Y&&o?(b(),q()):o||b(),document.addEventListener("visibilitychange",()=>{document.hidden||q()}),window.addEventListener("storage",q);const l=setInterval(q,1e3);return()=>{window.removeEventListener("storage",q),clearInterval(l)}});async function b(){if(!(v&&Y)){e(3,E=!0);try{const _=await(await fetch("/api/auth/local-token")).json();_.available?(e(7,F=!0),localStorage.setItem("hf_access_token",_.token),_.user_info&&_.user_info.name!=="Local User"?(e(1,v=!0),e(2,d=_.user_info.name.split(" ")[0])):(e(1,v=!0),e(2,d="Local User"))):e(7,F=!1)}catch{e(7,F=!1)}finally{e(3,E=!1)}}}function q(){const l=localStorage.getItem("hf_access_token"),_=localStorage.getItem("hf_user_info"),i=localStorage.getItem("hf_cached_token");if(l){if(l===i&&_)try{const a=JSON.parse(_);e(1,v=!0),e(2,d=a.username);return}catch{}(!v||l!==i)&&W(l)}else e(1,v=!1),e(2,d=""),localStorage.removeItem("hf_user_info"),localStorage.removeItem("hf_cached_token")}async function W(l){e(3,E=!0);try{const _=await fetch("https://huggingface.co/api/whoami-v2",{headers:{Authorization:`Bearer ${l}`}});if(_.ok){const i=await _.json();e(1,v=!0);const a=i.name||i.fullname||i.login||i.username||"User";e(2,d=a.split(" ")[0]);const S={username:d,fullName:a};localStorage.setItem("hf_user_info",JSON.stringify(S)),localStorage.setItem("hf_cached_token",l)}else localStorage.removeItem("hf_access_token"),localStorage.removeItem("hf_user_info"),localStorage.removeItem("hf_cached_token"),e(1,v=!1),e(2,d="")}catch{const i=localStorage.getItem("hf_user_info");if(i)try{const a=JSON.parse(i);e(1,v=!0),e(2,d=a.username);return}catch{}localStorage.removeItem("hf_access_token"),localStorage.removeItem("hf_user_info"),localStorage.removeItem("hf_cached_token"),e(1,v=!1),e(2,d="")}finally{e(3,E=!1)}}async function T(){if(v)localStorage.removeItem("hf_access_token"),localStorage.removeItem("hf_user_info"),localStorage.removeItem("hf_cached_token"),sessionStorage.removeItem("oauth_state"),e(1,v=!1),e(2,d="");else if(window.location.hostname.includes("hf.space")||window.location.hostname.includes("huggingface.co"))try{const _=await(await fetch("/api/auth/oauth-config")).json(),i=_.scopes||"read-repos write-repos manage-repos inference-api",a=`https://huggingface.co/oauth/authorize?client_id=${_.client_id}&redirect_uri=${encodeURIComponent(window.location.origin+"/auth/callback")}&scope=${encodeURIComponent(i)}&response_type=code&state=${Date.now()}`;window.location.href=a}catch{e(4,z=!0),e(5,p=""),e(6,I="")}else e(4,z=!0),e(5,p=""),e(6,I="")}function P(){e(4,z=!1),e(5,p=""),e(6,I="")}async function K(){if(!p.trim()){e(6,I="Please enter a token");return}if(!p.startsWith("hf_")){e(6,I='Token should start with "hf_"');return}try{const l=await fetch("https://huggingface.co/api/whoami-v2",{headers:{Authorization:`Bearer ${p.trim()}`}});if(l.ok){const _=await l.json(),i=p.trim();localStorage.setItem("hf_access_token",i),e(1,v=!0);const a=_.name||_.fullname||_.login||_.username||"User";e(2,d=a.split(" ")[0]);const S={username:d,fullName:a};localStorage.setItem("hf_user_info",JSON.stringify(S)),localStorage.setItem("hf_cached_token",i),P()}else e(6,I=`Invalid token (${l.status}). Please check your token and try again.`)}catch{e(6,I="Error validating token. Please try again.")}}const D=()=>e(0,k="tts");function V(){p=this.value,e(5,p)}const X=l=>l.key==="Enter"&&K();return c.$$set=l=>{"$$scope"in l&&e(12,h=l.$$scope)},[k,v,d,E,z,p,I,F,J,T,P,K,h,n,D,V,X]}class Ge extends Se{constructor(t){super(),Ce(this,t,Fe,ze,Le,{})}}export{Ge as component};
|
hfstudio/static/_app/immutable/nodes/1.i8TJb2hr.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{S as x,i as S,s as q,A as _,d as u,x as d,f as m,h as f,k as g,m as h,y as v,o as y,p as $,z as E,q as k,B as z}from"../chunks/BF3xBGh8.js";import"../chunks/IHki7fMi.js";import{s as A}from"../chunks/DVK2ASb7.js";const B=()=>{const s=A;return{page:{subscribe:s.page.subscribe},navigating:{subscribe:s.navigating.subscribe},updated:s.updated}},C={subscribe(s){return B().page.subscribe(s)}};function H(s){var b;let t,r=s[0].status+"",o,n,i,p=((b=s[0].error)==null?void 0:b.message)+"",l;return{c(){t=$("h1"),o=E(r),n=k(),i=$("p"),l=E(p)},l(e){t=g(e,"H1",{});var a=h(t);o=v(a,r),a.forEach(u),n=y(e),i=g(e,"P",{});var c=h(i);l=v(c,p),c.forEach(u)},m(e,a){m(e,t,a),f(t,o),m(e,n,a),m(e,i,a),f(i,l)},p(e,[a]){var c;a&1&&r!==(r=e[0].status+"")&&d(o,r),a&1&&p!==(p=((c=e[0].error)==null?void 0:c.message)+"")&&d(l,p)},i:_,o:_,d(e){e&&(u(t),u(n),u(i))}}}function P(s,t,r){let o;return z(s,C,n=>r(0,o=n)),[o]}let F=class extends x{constructor(t){super(),S(this,t,P,H,q,{})}};export{F as component};
|
hfstudio/static/_app/immutable/nodes/2.1jor8E63.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
hfstudio/static/_app/version.json
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
{"version":"
|
|
|
|
| 1 |
+
{"version":"1761004654684"}
|
hfstudio/static/index.html
CHANGED
|
@@ -1,35 +1,35 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
<html lang="en">
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
<link rel="modulepreload" href="/_app/immutable/entry/start.
|
| 10 |
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
| 11 |
<link rel="modulepreload" href="/_app/immutable/chunks/BF3xBGh8.js">
|
| 12 |
-
<link rel="modulepreload" href="/_app/immutable/entry/app.
|
| 13 |
<link rel="modulepreload" href="/_app/immutable/chunks/IHki7fMi.js">
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
<script>
|
| 18 |
{
|
| 19 |
-
|
| 20 |
base: ""
|
| 21 |
};
|
| 22 |
|
| 23 |
const element = document.currentScript.parentElement;
|
| 24 |
|
| 25 |
Promise.all([
|
| 26 |
-
import("/_app/immutable/entry/start.
|
| 27 |
-
import("/_app/immutable/entry/app.
|
| 28 |
]).then(([kit, app]) => {
|
| 29 |
kit.start(app, element);
|
| 30 |
});
|
| 31 |
}
|
| 32 |
</script>
|
| 33 |
</div>
|
| 34 |
-
|
| 35 |
-
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<link rel="icon" href="/assets/hf-studio-logo.png" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 7 |
+
<title>HFStudio - Text to Speech</title>
|
| 8 |
+
|
| 9 |
+
<link rel="modulepreload" href="/_app/immutable/entry/start.jV5w9ZfR.js">
|
| 10 |
+
<link rel="modulepreload" href="/_app/immutable/chunks/DVK2ASb7.js">
|
| 11 |
<link rel="modulepreload" href="/_app/immutable/chunks/BF3xBGh8.js">
|
| 12 |
+
<link rel="modulepreload" href="/_app/immutable/entry/app.CIwzfQ6B.js">
|
| 13 |
<link rel="modulepreload" href="/_app/immutable/chunks/IHki7fMi.js">
|
| 14 |
+
</head>
|
| 15 |
+
<body data-sveltekit-preload-data="hover">
|
| 16 |
+
<div style="display: contents">
|
| 17 |
<script>
|
| 18 |
{
|
| 19 |
+
__sveltekit_rw16zh = {
|
| 20 |
base: ""
|
| 21 |
};
|
| 22 |
|
| 23 |
const element = document.currentScript.parentElement;
|
| 24 |
|
| 25 |
Promise.all([
|
| 26 |
+
import("/_app/immutable/entry/start.jV5w9ZfR.js"),
|
| 27 |
+
import("/_app/immutable/entry/app.CIwzfQ6B.js")
|
| 28 |
]).then(([kit, app]) => {
|
| 29 |
kit.start(app, element);
|
| 30 |
});
|
| 31 |
}
|
| 32 |
</script>
|
| 33 |
</div>
|
| 34 |
+
</body>
|
| 35 |
+
</html>
|
models/chatterbox/local.py
CHANGED
|
@@ -33,7 +33,7 @@ class TTSRequest(BaseModel):
|
|
| 33 |
|
| 34 |
|
| 35 |
class InferenceClientTTSRequest(BaseModel):
|
| 36 |
-
inputs: str # text to synthesize
|
| 37 |
extra_body: Optional[Dict[str, Any]] = None
|
| 38 |
|
| 39 |
|
|
@@ -65,17 +65,17 @@ async def text_to_speech(request: TTSRequest):
|
|
| 65 |
"""
|
| 66 |
if not SAMPLE_AUDIO_PATH or not os.path.exists(SAMPLE_AUDIO_PATH):
|
| 67 |
raise HTTPException(
|
| 68 |
-
status_code=500,
|
| 69 |
-
detail="Sample audio file not found. Please provide --sample-audio path."
|
| 70 |
)
|
| 71 |
-
|
| 72 |
-
print(
|
| 73 |
-
|
|
|
|
|
|
|
| 74 |
# Return the sample audio file
|
| 75 |
return FileResponse(
|
| 76 |
-
SAMPLE_AUDIO_PATH,
|
| 77 |
-
media_type="audio/wav",
|
| 78 |
-
filename="generated_audio.wav"
|
| 79 |
)
|
| 80 |
|
| 81 |
|
|
@@ -87,48 +87,47 @@ async def inference_client_text_to_speech(request: InferenceClientTTSRequest):
|
|
| 87 |
"""
|
| 88 |
if not SAMPLE_AUDIO_PATH or not os.path.exists(SAMPLE_AUDIO_PATH):
|
| 89 |
raise HTTPException(
|
| 90 |
-
status_code=500,
|
| 91 |
-
detail="Sample audio file not found. Please provide --sample-audio path."
|
| 92 |
)
|
| 93 |
-
|
| 94 |
-
print(
|
| 95 |
-
|
|
|
|
|
|
|
| 96 |
# Return the sample audio file
|
| 97 |
return FileResponse(
|
| 98 |
-
SAMPLE_AUDIO_PATH,
|
| 99 |
-
media_type="audio/wav",
|
| 100 |
-
filename="generated_audio.wav"
|
| 101 |
)
|
| 102 |
|
| 103 |
|
| 104 |
def main():
|
| 105 |
global SAMPLE_AUDIO_PATH
|
| 106 |
-
|
| 107 |
parser = argparse.ArgumentParser(description="Start Chatterbox TTS Server")
|
| 108 |
-
parser.add_argument(
|
|
|
|
|
|
|
| 109 |
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
|
| 110 |
-
parser.add_argument(
|
| 111 |
-
|
|
|
|
|
|
|
| 112 |
args = parser.parse_args()
|
| 113 |
-
|
| 114 |
# Validate sample audio file exists
|
| 115 |
if not os.path.exists(args.sample_audio):
|
| 116 |
print(f"Error: Sample audio file not found: {args.sample_audio}")
|
| 117 |
exit(1)
|
| 118 |
-
|
| 119 |
SAMPLE_AUDIO_PATH = args.sample_audio
|
| 120 |
-
|
| 121 |
print(f"ποΈ Starting Chatterbox TTS Server on {args.host}:{args.port}")
|
| 122 |
print(f"π Using sample audio: {args.sample_audio}")
|
| 123 |
print(f"π API endpoint: http://localhost:{args.port}/")
|
| 124 |
-
|
| 125 |
-
uvicorn.run(
|
| 126 |
-
app,
|
| 127 |
-
host=args.host,
|
| 128 |
-
port=args.port,
|
| 129 |
-
log_level="info"
|
| 130 |
-
)
|
| 131 |
|
| 132 |
|
| 133 |
if __name__ == "__main__":
|
| 134 |
-
main()
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
class InferenceClientTTSRequest(BaseModel):
|
| 36 |
+
inputs: str # text to synthesize
|
| 37 |
extra_body: Optional[Dict[str, Any]] = None
|
| 38 |
|
| 39 |
|
|
|
|
| 65 |
"""
|
| 66 |
if not SAMPLE_AUDIO_PATH or not os.path.exists(SAMPLE_AUDIO_PATH):
|
| 67 |
raise HTTPException(
|
| 68 |
+
status_code=500,
|
| 69 |
+
detail="Sample audio file not found. Please provide --sample-audio path.",
|
| 70 |
)
|
| 71 |
+
|
| 72 |
+
print(
|
| 73 |
+
f"TTS Request - Text: '{request.inputs[:50]}...' Parameters: {request.parameters}"
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
# Return the sample audio file
|
| 77 |
return FileResponse(
|
| 78 |
+
SAMPLE_AUDIO_PATH, media_type="audio/wav", filename="generated_audio.wav"
|
|
|
|
|
|
|
| 79 |
)
|
| 80 |
|
| 81 |
|
|
|
|
| 87 |
"""
|
| 88 |
if not SAMPLE_AUDIO_PATH or not os.path.exists(SAMPLE_AUDIO_PATH):
|
| 89 |
raise HTTPException(
|
| 90 |
+
status_code=500,
|
| 91 |
+
detail="Sample audio file not found. Please provide --sample-audio path.",
|
| 92 |
)
|
| 93 |
+
|
| 94 |
+
print(
|
| 95 |
+
f"InferenceClient TTS Request - Text: '{request.inputs[:50]}...' Extra body: {request.extra_body}"
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
# Return the sample audio file
|
| 99 |
return FileResponse(
|
| 100 |
+
SAMPLE_AUDIO_PATH, media_type="audio/wav", filename="generated_audio.wav"
|
|
|
|
|
|
|
| 101 |
)
|
| 102 |
|
| 103 |
|
| 104 |
def main():
|
| 105 |
global SAMPLE_AUDIO_PATH
|
| 106 |
+
|
| 107 |
parser = argparse.ArgumentParser(description="Start Chatterbox TTS Server")
|
| 108 |
+
parser.add_argument(
|
| 109 |
+
"--port", "-p", type=int, default=7860, help="Port to run server on"
|
| 110 |
+
)
|
| 111 |
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
|
| 112 |
+
parser.add_argument(
|
| 113 |
+
"--sample-audio", required=True, help="Path to sample audio file to return"
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
args = parser.parse_args()
|
| 117 |
+
|
| 118 |
# Validate sample audio file exists
|
| 119 |
if not os.path.exists(args.sample_audio):
|
| 120 |
print(f"Error: Sample audio file not found: {args.sample_audio}")
|
| 121 |
exit(1)
|
| 122 |
+
|
| 123 |
SAMPLE_AUDIO_PATH = args.sample_audio
|
| 124 |
+
|
| 125 |
print(f"ποΈ Starting Chatterbox TTS Server on {args.host}:{args.port}")
|
| 126 |
print(f"π Using sample audio: {args.sample_audio}")
|
| 127 |
print(f"π API endpoint: http://localhost:{args.port}/")
|
| 128 |
+
|
| 129 |
+
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
|
| 132 |
if __name__ == "__main__":
|
| 133 |
+
main()
|
test_chatterbox.py
CHANGED
|
@@ -13,7 +13,7 @@ text = "In a hole in the ground there lived a hobbit."
|
|
| 13 |
extra_body = {
|
| 14 |
"exaggeration": 0.25,
|
| 15 |
"temperature": 0.7,
|
| 16 |
-
"audio_url": "https://huggingface.co/spaces/abidlabs/hfstudio/resolve/main/frontend/static/voices/lily.mp3"
|
| 17 |
}
|
| 18 |
|
| 19 |
bytes = client.text_to_speech(
|
|
@@ -22,4 +22,4 @@ bytes = client.text_to_speech(
|
|
| 22 |
extra_body=extra_body,
|
| 23 |
)
|
| 24 |
|
| 25 |
-
print(bytes)
|
|
|
|
| 13 |
extra_body = {
|
| 14 |
"exaggeration": 0.25,
|
| 15 |
"temperature": 0.7,
|
| 16 |
+
"audio_url": "https://huggingface.co/spaces/abidlabs/hfstudio/resolve/main/frontend/static/voices/lily.mp3",
|
| 17 |
}
|
| 18 |
|
| 19 |
bytes = client.text_to_speech(
|
|
|
|
| 22 |
extra_body=extra_body,
|
| 23 |
)
|
| 24 |
|
| 25 |
+
print(bytes)
|