|
|
<script> |
|
|
import '../app.css'; |
|
|
import { Home, Settings, Github, Menu, Mic, Layout, Code } from 'lucide-svelte'; |
|
|
import { onMount } from 'svelte'; |
|
|
import { page } from '$app/stores'; |
|
|
import Navbar from '$lib/components/Navbar.svelte'; |
|
|
|
|
|
let sidebarOpen = true; |
|
|
|
|
|
|
|
|
let initialUser = { authenticated: false }; |
|
|
let isLoggedIn = false; |
|
|
let username = ''; |
|
|
|
|
|
function setUserFromInitialData(userData) { |
|
|
initialUser = userData; |
|
|
isLoggedIn = userData?.authenticated || false; |
|
|
if (userData?.authenticated && userData?.user_info) { |
|
|
const userInfo = userData.user_info; |
|
|
const fullName = |
|
|
userInfo.name || userInfo.fullname || userInfo.login || userInfo.username || 'User'; |
|
|
username = fullName.split(' ')[0]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (typeof window !== 'undefined') { |
|
|
|
|
|
if (window.__INITIAL_USER__) { |
|
|
console.log('Found initial user data:', window.__INITIAL_USER__); |
|
|
setUserFromInitialData(window.__INITIAL_USER__); |
|
|
} else { |
|
|
console.log('No initial user data found, listening for event'); |
|
|
|
|
|
window.addEventListener('initial-user-loaded', (e) => { |
|
|
console.log('Received initial user event:', e.detail); |
|
|
setUserFromInitialData(e.detail); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
let showTokenInput = false; |
|
|
let tokenInput = ''; |
|
|
let tokenError = ''; |
|
|
let isLocalEnvironment = false; |
|
|
let showLoginPrompt = false; |
|
|
let flashButton = false; |
|
|
let isSpaces = false; |
|
|
|
|
|
onMount(() => { |
|
|
|
|
|
window.addEventListener('show-login-prompt', () => { |
|
|
if (!isLoggedIn) { |
|
|
showLoginPrompt = true; |
|
|
flashButton = true; |
|
|
setTimeout(() => { |
|
|
flashButton = false; |
|
|
}, 1600); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
checkSpacesStatus().then(() => { |
|
|
|
|
|
if (!window.__INITIAL_USER__ && !initialUser?.authenticated) { |
|
|
console.log('No server data available, checking auth status via API'); |
|
|
checkLoginStatus(); |
|
|
} |
|
|
}); |
|
|
|
|
|
document.addEventListener('visibilitychange', () => { |
|
|
if (!document.hidden) { |
|
|
checkLoginStatus(); |
|
|
} |
|
|
}); |
|
|
|
|
|
return () => { |
|
|
|
|
|
}; |
|
|
}); |
|
|
|
|
|
async function checkSpacesStatus() { |
|
|
try { |
|
|
const response = await fetch('/api/status'); |
|
|
const data = await response.json(); |
|
|
isSpaces = data.is_spaces || false; |
|
|
} catch (error) { |
|
|
console.error('Error checking Spaces status:', error); |
|
|
isSpaces = false; |
|
|
} |
|
|
} |
|
|
|
|
|
async function checkLocalTokenAvailability() { |
|
|
|
|
|
if (isLoggedIn) { |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/auth/local-token'); |
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.available) { |
|
|
isLocalEnvironment = true; |
|
|
|
|
|
|
|
|
if (data.token && !isLoggedIn) { |
|
|
try { |
|
|
const tokenResponse = await fetch('/api/auth/manual-token', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
credentials: 'include', |
|
|
body: JSON.stringify({ token: data.token }), |
|
|
}); |
|
|
|
|
|
if (tokenResponse.ok) { |
|
|
const tokenData = await tokenResponse.json(); |
|
|
isLoggedIn = true; |
|
|
const userInfo = tokenData.user_info; |
|
|
const fullName = |
|
|
userInfo.name || userInfo.fullname || userInfo.login || userInfo.username || 'User'; |
|
|
username = fullName.split(' ')[0]; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Local token session creation failed:', error); |
|
|
} |
|
|
} else if (data.user_info && data.user_info.name !== 'Local User' && !isLoggedIn) { |
|
|
isLoggedIn = true; |
|
|
username = data.user_info.name.split(' ')[0]; |
|
|
} else if (!isLoggedIn) { |
|
|
isLoggedIn = true; |
|
|
username = 'Local User'; |
|
|
} |
|
|
} else { |
|
|
isLocalEnvironment = false; |
|
|
} |
|
|
} catch (error) { |
|
|
isLocalEnvironment = false; |
|
|
} |
|
|
} |
|
|
|
|
|
async function checkLoginStatus() { |
|
|
|
|
|
if (initialUser?.authenticated && isLoggedIn) return; |
|
|
|
|
|
try { |
|
|
|
|
|
const response = await fetch('/api/auth/user', { |
|
|
credentials: 'include', |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
const data = await response.json(); |
|
|
if (data.authenticated) { |
|
|
isLoggedIn = true; |
|
|
const userInfo = data.user_info; |
|
|
const fullName = |
|
|
userInfo.name || userInfo.fullname || userInfo.login || userInfo.username || 'User'; |
|
|
username = fullName.split(' ')[0]; |
|
|
} else { |
|
|
isLoggedIn = false; |
|
|
username = ''; |
|
|
} |
|
|
} else { |
|
|
isLoggedIn = false; |
|
|
username = ''; |
|
|
} |
|
|
} catch (error) { |
|
|
isLoggedIn = false; |
|
|
username = ''; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function getPageTitle(pathname) { |
|
|
switch (pathname) { |
|
|
case '/': |
|
|
return 'Text to Speech Playground'; |
|
|
case '/voice-cloning': |
|
|
return 'Voice Cloning Playground'; |
|
|
default: |
|
|
return 'HFStudio'; |
|
|
} |
|
|
} |
|
|
|
|
|
async function handleAuthAction() { |
|
|
if (isLoggedIn) { |
|
|
|
|
|
try { |
|
|
await fetch('/api/auth/logout', { |
|
|
method: 'POST', |
|
|
credentials: 'include', |
|
|
}); |
|
|
} catch (error) { |
|
|
console.error('Logout error:', error); |
|
|
} |
|
|
|
|
|
|
|
|
sessionStorage.removeItem('oauth_state'); |
|
|
isLoggedIn = false; |
|
|
username = ''; |
|
|
|
|
|
|
|
|
window.location.reload(); |
|
|
} else { |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/auth/oauth-config'); |
|
|
const config = await response.json(); |
|
|
const scopes = config.scopes || 'inference-api'; |
|
|
|
|
|
|
|
|
let redirectUri = window.location.origin + '/auth/callback'; |
|
|
if (window.location.hostname === 'localhost' && window.location.port === '11111') { |
|
|
redirectUri = 'http://localhost:7860/auth/callback'; |
|
|
} |
|
|
|
|
|
|
|
|
const returnPath = window.location.pathname; |
|
|
|
|
|
const oauthUrl = `https://huggingface.co/oauth/authorize?client_id=${config.client_id}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scopes)}&response_type=code&state=${encodeURIComponent(returnPath)}`; |
|
|
window.location.href = oauthUrl; |
|
|
} catch (error) { |
|
|
|
|
|
showTokenInput = true; |
|
|
tokenInput = ''; |
|
|
tokenError = ''; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
function closeTokenInput() { |
|
|
showTokenInput = false; |
|
|
tokenInput = ''; |
|
|
tokenError = ''; |
|
|
} |
|
|
|
|
|
async function submitToken() { |
|
|
if (!tokenInput.trim()) { |
|
|
tokenError = 'Please enter a token'; |
|
|
return; |
|
|
} |
|
|
|
|
|
if (!tokenInput.startsWith('hf_')) { |
|
|
tokenError = 'Token should start with "hf_"'; |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch('https://huggingface.co/api/whoami-v2', { |
|
|
headers: { |
|
|
Authorization: `Bearer ${tokenInput.trim()}`, |
|
|
}, |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
const userData = await response.json(); |
|
|
const token = tokenInput.trim(); |
|
|
|
|
|
|
|
|
try { |
|
|
const tokenResponse = await fetch('/api/auth/manual-token', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
credentials: 'include', |
|
|
body: JSON.stringify({ token: token }), |
|
|
}); |
|
|
|
|
|
if (tokenResponse.ok) { |
|
|
const tokenData = await tokenResponse.json(); |
|
|
isLoggedIn = true; |
|
|
const userInfo = tokenData.user_info; |
|
|
const fullName = |
|
|
userInfo.name || userInfo.fullname || userInfo.login || userInfo.username || 'User'; |
|
|
username = fullName.split(' ')[0]; |
|
|
closeTokenInput(); |
|
|
} else { |
|
|
const errorData = await tokenResponse.json(); |
|
|
tokenError = errorData.detail || 'Token validation failed'; |
|
|
} |
|
|
} catch (error) { |
|
|
tokenError = 'Failed to validate token. Please try again.'; |
|
|
} |
|
|
} else { |
|
|
tokenError = `Invalid token (${response.status}). Please check your token and try again.`; |
|
|
} |
|
|
} catch (error) { |
|
|
tokenError = 'Error validating token. Please try again.'; |
|
|
} |
|
|
} |
|
|
</script> |
|
|
|
|
|
<div class="flex h-screen bg-white"> |
|
|
|
|
|
<aside |
|
|
class="w-56 border-r border-gray-200 bg-white flex-shrink-0 flex flex-col h-full relative {sidebarOpen |
|
|
? '' |
|
|
: 'hidden'}" |
|
|
> |
|
|
<div class="px-4 py-4 border-b border-gray-200 min-h-[73px] flex items-center"> |
|
|
<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> |
|
|
</div> |
|
|
|
|
|
<nav class="p-2 text-sm flex-1"> |
|
|
|
|
|
<div class="mt-2 mb-1 px-2 text-xs font-medium text-gray-500 uppercase">Audio</div> |
|
|
|
|
|
<a |
|
|
href="/" |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-100 transition-colors text-left |
|
|
{$page.url.pathname === '/' ? 'bg-gray-100' : ''}" |
|
|
> |
|
|
<span>🎙️</span> |
|
|
<span>Text to Speech</span> |
|
|
</a> |
|
|
|
|
|
<a |
|
|
href="/voice-cloning" |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-100 transition-colors text-left |
|
|
{$page.url.pathname === '/voice-cloning' ? 'bg-gray-100' : ''}" |
|
|
> |
|
|
<Mic size={16} /> |
|
|
<span>Voice Cloning</span> |
|
|
</a> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>🎧</span> |
|
|
<span>Speech to Text</span> |
|
|
</button> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>🎼</span> |
|
|
<span>Sound Effects</span> |
|
|
</button> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>🎸</span> |
|
|
<span>Music Generation</span> |
|
|
</button> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>🔊</span> |
|
|
<span>Audio Enhancement</span> |
|
|
</button> |
|
|
|
|
|
|
|
|
<div class="mt-4 mb-1 px-2 text-xs font-medium text-gray-500 uppercase">Image</div> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>🎨</span> |
|
|
<span>Text to Image</span> |
|
|
</button> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>🖼️</span> |
|
|
<span>Image to Image</span> |
|
|
</button> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>✂️</span> |
|
|
<span>Remove Background</span> |
|
|
</button> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>🔍</span> |
|
|
<span>Upscale Image</span> |
|
|
</button> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>🎭</span> |
|
|
<span>Face Swap</span> |
|
|
</button> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>📝</span> |
|
|
<span>Image to Text</span> |
|
|
</button> |
|
|
|
|
|
|
|
|
<div class="mt-4 mb-1 px-2 text-xs font-medium text-gray-500 uppercase">Video</div> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>🎬</span> |
|
|
<span>Text to Video</span> |
|
|
</button> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>🎞️</span> |
|
|
<span>Image to Video</span> |
|
|
</button> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>✨</span> |
|
|
<span>Video Enhancement</span> |
|
|
</button> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>🎤</span> |
|
|
<span>Lip Sync</span> |
|
|
</button> |
|
|
|
|
|
<button |
|
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed" |
|
|
disabled |
|
|
> |
|
|
<span>🗣️</span> |
|
|
<span>Video Dubbing</span> |
|
|
</button> |
|
|
</nav> |
|
|
</aside> |
|
|
|
|
|
|
|
|
<main class="flex-1 overflow-auto"> |
|
|
<Navbar |
|
|
{isLoggedIn} |
|
|
{username} |
|
|
{handleAuthAction} |
|
|
{flashButton} |
|
|
pageTitle={getPageTitle($page.url.pathname)} |
|
|
/> |
|
|
<slot /> |
|
|
</main> |
|
|
|
|
|
|
|
|
{#if showTokenInput} |
|
|
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> |
|
|
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4 shadow-xl"> |
|
|
<h2 class="text-xl font-semibold mb-4">Sign In with HuggingFace Token</h2> |
|
|
|
|
|
<div class="mb-4 p-3 bg-blue-50 rounded-md text-sm"> |
|
|
<p class="text-blue-800 mb-2"> |
|
|
<strong>Manual Token Entry:</strong> Please enter your HuggingFace token. |
|
|
</p> |
|
|
<p class="text-blue-700"> |
|
|
1. Go to <a |
|
|
href="https://huggingface.co/settings/tokens" |
|
|
target="_blank" |
|
|
class="underline text-blue-600">HuggingFace Settings</a |
|
|
><br /> |
|
|
2. Create a new token with "Inference API" permissions<br /> |
|
|
3. Copy and paste it below |
|
|
</p> |
|
|
{#if isLocalEnvironment} |
|
|
<p class="text-blue-600 mt-2"> |
|
|
<strong>Tip:</strong> You can also run <code>huggingface-cli login</code> in your terminal |
|
|
to automatically use your local token. |
|
|
</p> |
|
|
{/if} |
|
|
</div> |
|
|
|
|
|
<div class="mb-4"> |
|
|
<label for="token" class="block text-sm font-medium text-gray-700 mb-2"> |
|
|
HuggingFace Token |
|
|
</label> |
|
|
<input |
|
|
id="token" |
|
|
type="password" |
|
|
bind:value={tokenInput} |
|
|
placeholder="hf_..." |
|
|
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" |
|
|
on:keydown={(e) => e.key === 'Enter' && submitToken()} |
|
|
/> |
|
|
{#if tokenError} |
|
|
<p class="text-red-600 text-sm mt-1">{tokenError}</p> |
|
|
{/if} |
|
|
</div> |
|
|
|
|
|
<div class="flex justify-end gap-3"> |
|
|
<button |
|
|
on:click={closeTokenInput} |
|
|
class="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors" |
|
|
> |
|
|
Cancel |
|
|
</button> |
|
|
<button |
|
|
on:click={submitToken} |
|
|
class="px-4 py-2 bg-orange-500 text-white rounded-md hover:bg-orange-600 transition-colors" |
|
|
> |
|
|
Sign In |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
{/if} |
|
|
</div> |
|
|
|
|
|
<style> |
|
|
@keyframes sweep { |
|
|
from { |
|
|
transform: translateX(-100%); |
|
|
} |
|
|
to { |
|
|
transform: translateX(300%); |
|
|
} |
|
|
} |
|
|
|
|
|
.animate-sweep { |
|
|
animation: sweep 1.6s linear forwards; |
|
|
} |
|
|
</style> |
|
|
|