GitHub Action commited on
Commit
33b2a07
Β·
1 Parent(s): 1d29ab1

Sync from GitHub: b110eba71afe95ef3e43dd1d91c79c3e172e36f7

Browse files
.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*="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,7 +18,7 @@ pre[class*="language-"] {
18
  hyphens: none;
19
  }
20
 
21
- pre[class*="language-"] {
22
  padding: 1rem;
23
  margin: 0;
24
  overflow: auto;
@@ -35,7 +35,7 @@ pre[class*="language-"] {
35
  }
36
 
37
  .token.punctuation {
38
- color: #393A34;
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, #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,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
- <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>
 
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 = typeof window !== 'undefined' ? localStorage.getItem('hf_cached_token') : null;
12
- const initCachedUserInfo = typeof window !== 'undefined' ? localStorage.getItem('hf_user_info') : null;
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 !== "Local User") {
91
  isLoggedIn = true;
92
  username = data.user_info.name.split(' ')[0];
93
  } else {
94
  isLoggedIn = true;
95
- username = "Local User";
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
- 'Authorization': `Bearer ${token}`
144
- }
145
  });
146
-
147
  if (response.ok) {
148
  const userData = await response.json();
149
  isLoggedIn = true;
150
- const fullName = userData.name || userData.fullname || userData.login || userData.username || 'User';
 
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 (window.location.hostname.includes('hf.space') || window.location.hostname.includes('huggingface.co')) {
 
 
 
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
- 'Authorization': `Bearer ${tokenInput.trim()}`
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 = userData.name || userData.fullname || userData.login || userData.username || 'User';
 
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 class="w-56 border-r border-gray-200 bg-white flex-shrink-0 flex flex-col h-full {sidebarOpen ? '' : 'hidden'}">
 
 
 
 
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">HFStudio<sup class="text-xs text-gray-500 ml-1">BETA</sup></h1>
 
 
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
- Tasks
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
- <div class="absolute bottom-full left-0 right-0 mb-2 z-50">
351
- <div class="bg-blue-600 text-white text-sm rounded-lg p-3 shadow-lg relative">
352
- <div class="flex items-start gap-2">
353
- <svg class="w-4 h-4 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
354
- <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"/>
355
- </svg>
356
- <div>
357
- <p class="font-medium">Sign in required</p>
358
- <p class="text-blue-100 text-xs mt-1">You need to sign in to use HuggingFace Inference Providers for text-to-speech generation.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 href="https://huggingface.co/settings/tokens" target="_blank" class="underline text-blue-600">HuggingFace Settings</a><br>
388
- 2. Create a new token with "Inference API" permissions<br>
 
 
 
 
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 to automatically use your local token.
 
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 { Play, Download, Loader2, AlertCircle, ChevronDown, Copy, RefreshCw, Share, MoreHorizontal, Settings, Sliders, Pause, SkipBack, SkipForward, Layout, Code, X, RotateCcw } from 'lucide-svelte';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- { id: 'lily', name: 'Lily', description: 'Warm, conversational voice from a female in her 30s', sample: '/voices/lily.mp3' },
43
- { id: 'andrew', name: 'Andrew', description: 'Older British man who speaks clearly and kindly', sample: '/voices/andrew.mp3' },
44
- { id: 'fairy', name: 'Fairy', description: 'High and airy female voice that bursts with excitement', sample: '/voices/fairy.mp3' },
45
- { id: 'pirate', name: 'Pirate', description: 'Young pirate that speaks gruffly and passionately', sample: '/voices/pirate.mp3' },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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('Network Error', 'Failed to connect to the server. Please check your connection and try again.');
 
 
 
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: "https://huggingface.co/spaces/Abradi/hfstudio/resolve/main/voices/lily.mp3",
375
- andrew: "https://huggingface.co/spaces/Abradi/hfstudio/resolve/main/voices/andrew.mp3",
376
- fairy: "https://huggingface.co/spaces/Abradi/hfstudio/resolve/main/voices/fairy.mp3",
377
- pirate: "https://huggingface.co/spaces/Abradi/hfstudio/resolve/main/voices/pirate.mp3"
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
- 'Authorization': `Bearer ${token}`
483
- }
484
  });
485
-
486
  if (response.ok) {
487
  const userData = await response.json();
488
- currentUsername = userData.name || userData.fullname || userData.login || userData.username || 'User';
 
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 === 'ui' ? 'bg-white shadow-sm' : 'text-gray-600'}"
557
- on:click={() => viewMode = 'ui'}
 
 
 
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 === 'code' ? 'bg-white shadow-sm' : 'text-gray-600'} {codeButtonFlash ? 'code-flash' : ''}"
564
- on:click={() => viewMode = 'code'}
 
 
 
565
  >
566
  <Code size={14} />
567
  Code Recorder
568
  {#if codeButtonFlash}
569
- <span class="flash-sweep"></span>
570
  {/if}
571
  </button>
572
  </div>
573
  </div>
574
  </div>
575
  </header>
576
-
577
  <!-- Main content area -->
578
  {#if viewMode === 'ui'}
579
- <div class="flex-1 flex">
580
- <!-- Main content area -->
581
- <div class="flex-1 flex flex-col p-6">
582
-
583
- <!-- Text input area -->
584
- <div class="flex-1 pb-24">
585
- <textarea
586
- bind:value={text}
587
- class="w-full h-full p-6 bg-white resize-none border-0 focus:outline-none text-gray-900 text-base leading-relaxed"
588
- placeholder="In a hole in the ground there lived a hobbit."
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
- <!-- Mini audio controls -->
643
- <div class="flex items-center gap-3 mb-4">
644
- <!-- Play/Pause button -->
645
- <button
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
- </div>
668
-
669
- <!-- Full audio player controls -->
670
- <div class="flex items-center gap-4 mb-4">
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 isPlaying}
682
- <div class="pause-filled text-white scale-150"></div>
 
683
  {:else}
684
- <Play size={20} class="text-white ml-0.5" />
 
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
- <!-- Hidden audio element -->
728
  {#if audioUrl}
729
- <audio
730
- bind:this={audioElement}
731
- src={audioUrl}
732
- on:loadedmetadata={handleAudioLoad}
733
- on:timeupdate={handleTimeUpdate}
734
- on:play={handlePlay}
735
- on:pause={handlePause}
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}&nbsp;<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}&nbsp;<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
- <div class="flex-1 min-w-0">
804
- <div class="text-sm font-medium text-gray-900 mb-1">{voice.name}</div>
805
- <div class="text-xs text-gray-500 leading-relaxed">
806
- {voice.description}
807
- </div>
 
 
 
 
 
 
 
 
 
 
 
808
  </div>
809
  </div>
810
- <button
811
- on:click={(e) => playSampleVoice(voice, e)}
812
- class="p-2 rounded-full hover:bg-gray-200 transition-colors flex-shrink-0 ml-2"
813
- title="Play sample"
814
- >
815
- {#if playingSampleVoice === voice.name}
816
- <Pause size={16} class="text-gray-600" />
817
- {:else}
818
- <Play size={16} class="text-gray-600" />
819
- {/if}
820
- </button>
821
- </button>
822
- {/each}
823
-
824
- <!-- Clone voice option -->
825
- <button
826
- class="w-full flex items-center justify-between p-2 rounded-lg opacity-50 cursor-not-allowed text-left border border-transparent"
827
- disabled
828
- >
829
- <div class="flex items-center gap-3 flex-1 min-w-0">
830
- <div class="w-10 h-10 bg-gray-400 rounded-full flex items-center justify-center text-white text-sm font-medium flex-shrink-0">
831
- +
 
 
 
832
  </div>
833
- <div class="flex-1 min-w-0">
834
- <div class="text-sm font-medium text-gray-600 mb-1">Clone your voice</div>
835
- <div class="text-xs text-gray-400">
836
- (coming soon)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
838
  </div>
839
  </div>
840
- </button>
841
- </div>
842
- </div>
843
-
844
- <div class="space-y-4 pt-4 border-t border-gray-200">
845
- <!-- Exaggeration control -->
846
- <div>
847
- <div class="flex justify-between mb-1">
848
- <label for="exaggeration-slider" class="text-sm font-medium text-gray-700">Exaggeration</label>
849
- <span class="text-sm text-gray-500">{exaggeration.toFixed(2)}</span>
850
- </div>
851
- <input
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
- {#if codeHistory.length > 0 || setupCode || importCode}
925
- <div class="flex items-center gap-2">
926
- <button
927
- on:click={resetHistory}
928
- class="flex items-center bg-red-50 hover:bg-red-100 rounded-md px-3 py-1.5 transition-colors"
929
- title="Clear history"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
  >
931
- <RotateCcw size={16} class="text-red-600" />
932
- <span class="ml-2 text-sm font-medium text-red-600">Reset</span>
933
- </button>
934
- <button
935
- on:click={copyAllCode}
936
- class="flex items-center bg-gray-100 hover:bg-gray-200 rounded-md px-3 py-1.5 transition-colors"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <!-- Code sections -->
947
- {#if !setupCode && !importCode && codeHistory.length === 0}
948
- <div class="bg-white rounded-lg border border-gray-200 p-8 text-center">
949
- <p class="text-gray-500">Start using the UI to see generated code here</p>
950
- {#if currentUsername}
951
- <p class="text-xs text-gray-400 mt-2">Logged in as: {currentUsername}</p>
952
- {/if}
953
- </div>
954
- {:else}
955
- <div class="space-y-6">
956
- <!-- Setup Section -->
957
- {#if setupCode}
958
- <div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
959
- <div class="flex items-center justify-between px-4 py-2 bg-amber-50 border-b border-amber-200">
960
- <div class="flex items-center gap-2">
961
- <span class="text-sm font-medium text-amber-900">Setup (Terminal)</span>
962
- <span class="text-xs bg-amber-100 text-amber-700 px-2 py-0.5 rounded">Run once</span>
963
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
964
  <button
965
- on:click={() => copyToClipboard(setupCode)}
966
- class="p-1.5 hover:bg-amber-100 rounded transition-colors"
967
- title="Copy setup code"
968
  >
969
- <Copy size={14} class="text-amber-600" />
 
 
 
 
 
 
 
 
 
 
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
- {/if}
977
-
978
- <!-- Import Section -->
979
- {#if importCode}
980
- <div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
981
- <div class="flex items-center justify-between px-4 py-2 bg-blue-50 border-b border-blue-200">
982
- <div class="flex items-center gap-2">
983
- <span class="text-sm font-medium text-blue-900">Imports (Python)</span>
984
- <span class="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded">Run once</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <div class="relative">
995
- <pre class="p-4 overflow-x-auto bg-gray-50"><code class="language-python text-sm">{@html Prism.highlight(importCode, Prism.languages.python, 'python')}</code></pre>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
996
  </div>
997
  </div>
998
- {/if}
999
-
1000
- <!-- History entries -->
1001
- {#each codeHistory as entry, i (entry.id)}
1002
- <div class="bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm">
1003
- <!-- Code cell -->
1004
- <div class="border-b border-gray-200">
1005
- <div class="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-100">
1006
- <span class="text-sm font-medium text-gray-700">Cell {i + 1}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1007
  <button
1008
- on:click={() => copyToClipboard(entry.code)}
1009
- class="p-1.5 hover:bg-gray-200 rounded transition-colors"
1010
- title="Copy code"
 
1011
  >
1012
- <Copy size={14} class="text-gray-600" />
 
 
 
 
 
 
 
 
1013
  </button>
1014
  </div>
1015
- <div class="relative">
1016
- <pre class="p-4 overflow-x-auto bg-gray-50"><code class="language-python text-sm">{@html Prism.highlight(entry.code, Prism.languages.python, 'python')}</code></pre>
1017
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1018
  </div>
1019
-
1020
- <!-- Result (audio player) -->
1021
- {#if entry.result && entry.result.type === 'audio'}
1022
- <div class="bg-gradient-to-b from-gray-50 to-white p-4">
1023
- <div class="bg-white rounded-lg border border-gray-200 p-4 shadow-sm">
1024
- <div class="flex items-center justify-between">
1025
- <div class="flex items-center gap-3 flex-1">
 
 
 
 
 
 
 
1026
  <button
1027
- on:click={() => toggleHistoryAudio(entry)}
1028
- 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"
 
1029
  >
1030
- {#if entry.isPlaying}
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="flex items-center gap-1">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1042
  <button
1043
- on:click={() => downloadHistoryAudio(entry.result.url, entry.result.title)}
1044
- class="p-2 hover:bg-gray-100 rounded-lg transition-colors"
1045
- title="Download"
1046
  >
1047
- <Download size={16} class="text-gray-600" />
1048
  </button>
1049
- <button
1050
- class="p-2 hover:bg-gray-100 rounded-lg transition-colors"
1051
- title="Share"
 
 
 
 
 
 
 
 
 
 
 
 
 
1052
  >
1053
- <Share size={16} class="text-gray-600" />
1054
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
1055
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1056
  </div>
1057
- <audio
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
- {/if}
1066
- </div>
1067
- {/each}
1068
  </div>
1069
- {/if}
1070
  </div>
1071
- </div>
1072
  {/if}
1073
-
1074
  <!-- Copy notification toast -->
1075
  {#if copyNotification}
1076
- <div class="fixed bottom-4 right-4 px-4 py-2 bg-gray-900 text-white rounded-lg shadow-lg z-50 animate-fade-in">
1077
- {copyNotification}
1078
- </div>
 
 
1079
  {/if}
1080
 
1081
  <!-- Error Modal -->
1082
  {#if showErrorModal}
1083
- <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
1084
- <div class="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[80vh] flex flex-col">
1085
- <!-- Header -->
1086
- <div class="flex items-center justify-between p-6 border-b border-gray-200 bg-red-50 flex-shrink-0">
1087
- <div class="flex items-center gap-3 min-w-0">
1088
- <div class="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center flex-shrink-0">
1089
- <AlertCircle size={20} class="text-red-600" />
1090
- </div>
1091
- <div class="min-w-0">
1092
- <h3 class="text-lg font-semibold text-gray-900 truncate">{errorMessage}</h3>
1093
- <p class="text-sm text-gray-600">An error occurred while processing your request</p>
 
 
 
 
 
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
- <!-- Content -->
1106
- <div class="p-6 overflow-y-auto flex-1 min-h-0">
1107
- {#if errorDetails}
1108
- <div class="bg-gray-50 rounded-lg p-4 border">
1109
- <h4 class="text-sm font-medium text-gray-900 mb-2">Error Details:</h4>
1110
- <pre class="text-xs text-gray-700 whitespace-pre-wrap font-mono leading-relaxed break-words">{errorDetails}</pre>
 
 
 
1111
  </div>
1112
- {/if}
1113
- </div>
1114
 
1115
- <!-- Footer -->
1116
- <div class="flex items-center justify-end gap-3 p-6 border-t border-gray-200 bg-gray-50 flex-shrink-0">
1117
- <button
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
- <Copy size={16} />
1122
- Copy Error
1123
- </button>
1124
- <button
1125
- on:click={closeErrorModal}
1126
- class="px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
1127
- >
1128
- Close
1129
- </button>
 
 
 
 
 
1130
  </div>
1131
  </div>
1132
- </div>
1133
  {/if}
1134
  </div>
1135
 
1136
  <style>
1137
  @keyframes fade-in {
1138
- from { opacity: 0; transform: translateY(10px); }
1139
- to { opacity: 1; transform: translateY(0); }
 
 
 
 
 
 
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(90deg,
1168
- transparent 0%,
 
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}&nbsp;<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}&nbsp;<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, 'r') as f:
14
  package_data = json.load(f)
15
- return package_data['version']
 
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, 'r') as f:
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(f"[yellow]Warning: Invalid JSON in {spec_file}[/yellow]")
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", "run", str(full_script_path),
122
- "--port", str(port),
123
- "--host", host,
124
- "--sample-audio", sample_audio
 
 
 
 
 
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(Panel.fit(
143
- "[bold yellow]πŸŽ™οΈ HFStudio CLI[/bold yellow]\n\n"
144
- "[green]Available Commands:[/green]\n"
145
- "β€’ [cyan]hfstudio start[/cyan] <model> - Start a TTS model locally\n"
146
- "β€’ [cyan]hfstudio list[/cyan] - List available models\n"
147
- "β€’ [cyan]hfstudio dev-server[/cyan] - Start development server\n"
148
- "β€’ [cyan]hfstudio --help[/cyan] - Show detailed help\n\n"
149
- "[dim]Example: hfstudio start resambleai/chatterbox --port 1234[/dim]",
150
- title="πŸŽ™οΈ HFStudio",
151
- border_style="yellow"
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(Panel.fit(
187
- "[bold green]HFStudio Development Server[/bold green]\n"
188
- f"Running on http://{host if host != '0.0.0.0' else 'localhost'}:{port}\n"
189
- f"API docs: http://localhost:{port}/docs",
190
- title="πŸŽ™οΈ HFStudio Dev Server",
191
- border_style="green"
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, 'r') as f:
42
  return json.load(f)
43
  except (json.JSONDecodeError, IOError):
44
  return None
45
  return None
46
 
47
- def generate_tts_with_client(client: InferenceClient, request: TTSRequest, model_spec: Dict[str, Any]) -> TTSResponse:
 
 
 
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('utf-8')
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("/assets", StaticFiles(directory=str(static_dir / "assets")), name="assets")
104
- app.mount("/samples", StaticFiles(directory=str(static_dir / "samples")), name="samples")
 
 
 
 
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
- "mode": "api",
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("OAUTH_SCOPES", "read-repos write-repos manage-repos inference-api")
153
-
 
 
154
  return {
155
- "client_id": os.getenv("OAUTH_CLIENT_ID", "cdf32a17-e40f-4a84-b683-f66aa1105793"),
 
 
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 {"available": False, "reason": f"token_validation_error: {str(api_error)}"}
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", {}).get("voice_urls", {}).get(voice_spec["id"]),
216
- supported_models=[model_dir.name]
 
 
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("endpoint_model", request.model_id)
 
 
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("OAUTH_CLIENT_SECRET", "f590cb2d-6eac-4cef-a0cb-d0116825295c")
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
- """, status_code=400)
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("OAUTH_CLIENT_SECRET", "f590cb2d-6eac-4cef-a0cb-d0116825295c")
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(f"""
 
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
- """, status_code=400)
415
-
 
 
416
  except Exception as e:
417
- return HTMLResponse(f"""
 
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
- """, status_code=500)
 
 
 
427
 
428
  @app.get("/{path:path}")
429
  async def serve_spa(path: str):
430
- if path.startswith("api/") or path.startswith("docs") or path.startswith("openapi.json"):
 
 
 
 
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
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
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 &quot;Inference API&quot; 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":"1761004481087"}
 
1
+ {"version":"1761004654684"}
hfstudio/static/index.html CHANGED
@@ -1,35 +1,35 @@
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.CSDWo18g.js">
10
- <link rel="modulepreload" href="/_app/immutable/chunks/58WuCvUx.js">
11
  <link rel="modulepreload" href="/_app/immutable/chunks/BF3xBGh8.js">
12
- <link rel="modulepreload" href="/_app/immutable/entry/app.pFM9bQvm.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_7hn0aa = {
20
  base: ""
21
  };
22
 
23
  const element = document.currentScript.parentElement;
24
 
25
  Promise.all([
26
- import("/_app/immutable/entry/start.CSDWo18g.js"),
27
- import("/_app/immutable/entry/app.pFM9bQvm.js")
28
  ]).then(([kit, app]) => {
29
  kit.start(app, element);
30
  });
31
  }
32
  </script>
33
  </div>
34
- </body>
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(f"TTS Request - Text: '{request.inputs[:50]}...' Parameters: {request.parameters}")
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(f"InferenceClient TTS Request - Text: '{request.inputs[:50]}...' Extra body: {request.extra_body}")
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("--port", "-p", type=int, default=7860, help="Port to run server on")
 
 
109
  parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
110
- parser.add_argument("--sample-audio", required=True, help="Path to sample audio file to return")
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)