|
|
<script> |
|
|
import { Copy, RotateCcw, Play, Download, Share, Pause, Layout, Code } from 'lucide-svelte'; |
|
|
import { onMount } from 'svelte'; |
|
|
import Prism from 'prismjs'; |
|
|
import 'prismjs/components/prism-python'; |
|
|
import 'prismjs/components/prism-bash'; |
|
|
import Navbar from '$lib/components/Navbar.svelte'; |
|
|
|
|
|
let selectedModel = 'Chatterbox'; |
|
|
let mode = 'api'; |
|
|
let codeHistory = []; |
|
|
let setupCode = generateSetupCode(); |
|
|
let importCode = generateImportCode(); |
|
|
let copyNotification = null; |
|
|
let historyCount = 0; |
|
|
|
|
|
const models = [ |
|
|
{ id: 'chatterbox', name: 'Chatterbox', badge: 'recommended' }, |
|
|
{ id: 'kokoro', name: 'Kokoro', badge: 'coming soon', disabled: true }, |
|
|
]; |
|
|
|
|
|
const voices = [ |
|
|
{ |
|
|
id: 'andrew', |
|
|
name: 'Andrew', |
|
|
description: 'Older British man who speaks clearly and kindly', |
|
|
sample: '/voices/andrew.mp3', |
|
|
preview_url: |
|
|
'https://huggingface.co/spaces/abidlabs/hfstudio/resolve/main/frontend/static/voices/andrew.mp3', |
|
|
}, |
|
|
{ |
|
|
id: 'lily', |
|
|
name: 'Jasmine', |
|
|
description: 'Warm, conversational tone of a woman in her 30s', |
|
|
sample: '/voices/lily.mp3', |
|
|
preview_url: |
|
|
'https://huggingface.co/spaces/abidlabs/hfstudio/resolve/main/frontend/static/voices/lily.mp3', |
|
|
}, |
|
|
{ |
|
|
id: 'pirate', |
|
|
name: 'Pirate', |
|
|
description: 'Young male pirate-y voice that speaks gruffly and with excitement', |
|
|
sample: '/voices/pirate.mp3', |
|
|
preview_url: |
|
|
'https://huggingface.co/spaces/abidlabs/hfstudio/resolve/main/frontend/static/voices/pirate.mp3', |
|
|
}, |
|
|
{ |
|
|
id: 'fairy', |
|
|
name: 'Fairy', |
|
|
description: 'High and airy female voice that bursts with excitement', |
|
|
sample: '/voices/fairy.mp3', |
|
|
preview_url: |
|
|
'https://huggingface.co/spaces/abidlabs/hfstudio/resolve/main/frontend/static/voices/fairy.mp3', |
|
|
}, |
|
|
]; |
|
|
|
|
|
async function loadHistoryFromDatabase() { |
|
|
try { |
|
|
const response = await fetch('/api/history/load', { |
|
|
method: 'GET', |
|
|
credentials: 'include', |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
const data = await response.json(); |
|
|
|
|
|
const setupEntries = data.entries.filter((e) => e.entry_type === 'setup'); |
|
|
const importEntries = data.entries.filter((e) => e.entry_type === 'import'); |
|
|
const generationEntries = data.entries.filter((e) => e.entry_type === 'generation'); |
|
|
|
|
|
setupCode = generateSetupCode(); |
|
|
importCode = |
|
|
importEntries.length > 0 |
|
|
? importEntries[importEntries.length - 1].code |
|
|
: generateImportCode(); |
|
|
|
|
|
codeHistory = generationEntries.map((entry) => ({ |
|
|
id: entry.id, |
|
|
code: entry.code, |
|
|
result: entry.result_data, |
|
|
})); |
|
|
|
|
|
historyCount = generationEntries.length; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error loading history from database:', error); |
|
|
codeHistory = []; |
|
|
setupCode = generateSetupCode(); |
|
|
importCode = generateImportCode(); |
|
|
} |
|
|
} |
|
|
|
|
|
async function resetHistory() { |
|
|
try { |
|
|
await fetch('/api/history/clear', { |
|
|
method: 'DELETE', |
|
|
credentials: 'include', |
|
|
}); |
|
|
|
|
|
codeHistory = []; |
|
|
setupCode = generateSetupCode(); |
|
|
importCode = generateImportCode(); |
|
|
historyCount = 0; |
|
|
} catch (error) { |
|
|
console.error('Error clearing history:', error); |
|
|
codeHistory = []; |
|
|
setupCode = generateSetupCode(); |
|
|
importCode = generateImportCode(); |
|
|
historyCount = 0; |
|
|
} |
|
|
} |
|
|
|
|
|
function generateSetupCode() { |
|
|
if (mode === 'local') { |
|
|
return `pip install huggingface-hub hfstudio uv |
|
|
hfstudio start ${selectedModel.toLowerCase()} --port 7861`; |
|
|
} else { |
|
|
return `pip install huggingface-hub`; |
|
|
} |
|
|
} |
|
|
|
|
|
function generateClientInitCode() { |
|
|
if (mode === 'local') { |
|
|
const port = 7861; |
|
|
return `client = InferenceClient(base_url="http://localhost:${port}/api/v1")`; |
|
|
} else { |
|
|
const endpointModel = |
|
|
selectedModel.toLowerCase() === 'chatterbox' |
|
|
? 'ResembleAI/chatterbox' |
|
|
: selectedModel.toLowerCase(); |
|
|
return `client = InferenceClient( |
|
|
api_key="YOUR_HF_TOKEN", # Get your token from https://huggingface.co/settings/tokens |
|
|
model="${endpointModel}", |
|
|
)`; |
|
|
} |
|
|
} |
|
|
|
|
|
function generateImportCode() { |
|
|
const clientCode = generateClientInitCode(); |
|
|
|
|
|
if (mode === 'local') { |
|
|
return `from huggingface_hub import InferenceClient |
|
|
|
|
|
${clientCode}`; |
|
|
} else { |
|
|
return `from huggingface_hub import InferenceClient |
|
|
|
|
|
${clientCode}`; |
|
|
} |
|
|
} |
|
|
|
|
|
function copyToClipboard(text, message = 'Copied to clipboard!') { |
|
|
navigator.clipboard.writeText(text).then(() => { |
|
|
copyNotification = message; |
|
|
setTimeout(() => { |
|
|
copyNotification = null; |
|
|
}, 2000); |
|
|
}); |
|
|
} |
|
|
|
|
|
function copyAllCode() { |
|
|
const parts = []; |
|
|
|
|
|
if (setupCode) { |
|
|
const isTerminalCommand = |
|
|
setupCode.includes('pip install') || setupCode.includes('hfstudio start'); |
|
|
const language = isTerminalCommand ? 'bash' : ''; |
|
|
parts.push(`## Setup (Run in Terminal)\n\n\`\`\`${language}\n${setupCode}\n\`\`\``); |
|
|
} |
|
|
|
|
|
if (importCode) { |
|
|
parts.push(`## Imports (Python)\n\n\`\`\`python\n${importCode}\n\`\`\``); |
|
|
} |
|
|
|
|
|
codeHistory.forEach((entry, i) => { |
|
|
parts.push(`## Cell ${i + 1}\n\n\`\`\`python\n${entry.code}\n\`\`\``); |
|
|
}); |
|
|
|
|
|
const markdownContent = parts.join('\n\n'); |
|
|
copyToClipboard(markdownContent, 'All code copied as Markdown!'); |
|
|
} |
|
|
|
|
|
function toggleHistoryAudio(entry) { |
|
|
if (!entry.audioElement) { |
|
|
entry.audioElement = new Audio(entry.result.url); |
|
|
entry.audioElement.addEventListener('ended', () => { |
|
|
entry.isPlaying = false; |
|
|
codeHistory = [...codeHistory]; |
|
|
}); |
|
|
} |
|
|
|
|
|
if (entry.isPlaying) { |
|
|
entry.audioElement.pause(); |
|
|
entry.isPlaying = false; |
|
|
} else { |
|
|
codeHistory.forEach((e) => { |
|
|
if (e !== entry && e.isPlaying && e.audioElement) { |
|
|
e.audioElement.pause(); |
|
|
e.isPlaying = false; |
|
|
} |
|
|
}); |
|
|
entry.audioElement.play(); |
|
|
entry.isPlaying = true; |
|
|
} |
|
|
codeHistory = [...codeHistory]; |
|
|
} |
|
|
|
|
|
function downloadHistoryAudio(url, title) { |
|
|
const link = document.createElement('a'); |
|
|
link.href = url; |
|
|
link.download = `${title || 'audio'}.wav`; |
|
|
document.body.appendChild(link); |
|
|
link.click(); |
|
|
document.body.removeChild(link); |
|
|
} |
|
|
|
|
|
function formatDuration(seconds) { |
|
|
if (!seconds) return '0:00'; |
|
|
const mins = Math.floor(seconds / 60); |
|
|
const secs = Math.floor(seconds % 60); |
|
|
return `${mins}:${secs.toString().padStart(2, '0')}`; |
|
|
} |
|
|
|
|
|
$: if (mode) { |
|
|
setupCode = generateSetupCode(); |
|
|
importCode = generateImportCode(); |
|
|
} |
|
|
|
|
|
onMount(async () => { |
|
|
await loadHistoryFromDatabase(); |
|
|
}); |
|
|
</script> |
|
|
|
|
|
<svelte:head> |
|
|
<title>Code Recorder - HFStudio</title> |
|
|
</svelte:head> |
|
|
|
|
|
<div class="flex-1 bg-gray-50 overflow-y-auto"> |
|
|
<Navbar {historyCount} /> |
|
|
|
|
|
<div class="max-w-4xl mx-auto p-8"> |
|
|
|
|
|
<div class="mb-6"> |
|
|
<div> |
|
|
<h2 class="text-2xl font-semibold text-gray-900">Code Recorder</h2> |
|
|
<p class="text-sm text-gray-600 mt-1"> |
|
|
{#if mode === 'local'} |
|
|
Python code to reproduce your actions using a local HFStudio server |
|
|
{:else} |
|
|
Python code to reproduce your actions via the API |
|
|
{/if} |
|
|
</p> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="flex items-center justify-between mt-4"> |
|
|
|
|
|
<div class="flex items-center bg-gray-100 rounded-md p-0.5"> |
|
|
<button |
|
|
class="px-3 py-1 text-sm font-medium rounded transition-colors {mode === 'api' |
|
|
? 'bg-white shadow-sm' |
|
|
: 'text-gray-600'}" |
|
|
on:click={() => (mode = 'api')} |
|
|
> |
|
|
API |
|
|
</button> |
|
|
<button |
|
|
class="px-3 py-1 text-sm font-medium rounded transition-colors {mode === 'local' |
|
|
? 'bg-white shadow-sm' |
|
|
: 'text-gray-600'}" |
|
|
on:click={() => (mode = 'local')} |
|
|
> |
|
|
Local |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
{#if codeHistory.length > 0 || setupCode || importCode} |
|
|
<div class="flex items-center gap-2"> |
|
|
<button |
|
|
on:click={resetHistory} |
|
|
class="flex items-center bg-red-50 hover:bg-red-100 rounded-md px-3 py-1.5 transition-colors" |
|
|
title="Clear history" |
|
|
> |
|
|
<RotateCcw size={16} class="text-red-600" /> |
|
|
<span class="ml-2 text-sm font-medium text-red-600">Reset history</span> |
|
|
</button> |
|
|
<button |
|
|
on:click={copyAllCode} |
|
|
class="flex items-center bg-gray-100 hover:bg-gray-200 rounded-md px-3 py-1.5 transition-colors" |
|
|
> |
|
|
<Copy size={16} class="text-gray-600" /> |
|
|
<span class="ml-2 text-sm font-medium text-gray-600">Copy all as Markdown</span> |
|
|
</button> |
|
|
</div> |
|
|
{/if} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Code sections --> |
|
|
<div class="space-y-6"> |
|
|
|
|
|
{#if setupCode} |
|
|
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden"> |
|
|
<div |
|
|
class="flex items-center justify-between px-4 py-2 bg-amber-50 border-b border-amber-200" |
|
|
> |
|
|
<div class="flex items-center gap-2"> |
|
|
<span class="text-sm font-medium text-amber-900">Setup (Run in Terminal)</span> |
|
|
<span class="text-xs bg-amber-100 text-amber-700 px-2 py-0.5 rounded">Run once</span> |
|
|
</div> |
|
|
<button |
|
|
on:click={() => copyToClipboard(setupCode)} |
|
|
class="p-1.5 hover:bg-amber-100 rounded transition-colors" |
|
|
title="Copy setup code" |
|
|
> |
|
|
<Copy size={14} class="text-amber-600" /> |
|
|
</button> |
|
|
</div> |
|
|
<div class="relative"> |
|
|
{#if setupCode === 'pip install huggingface-hub'} |
|
|
<pre class="p-4 overflow-x-auto bg-gray-50"><code |
|
|
class="language-bash text-sm text-black">{setupCode}</code |
|
|
></pre> |
|
|
{:else} |
|
|
<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> |
|
|
{/if} |
|
|
</div> |
|
|
</div> |
|
|
{/if} |
|
|
|
|
|
<!-- Import Section --> |
|
|
{#if importCode} |
|
|
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden"> |
|
|
<div |
|
|
class="flex items-center justify-between px-4 py-2 bg-blue-50 border-b border-blue-200" |
|
|
> |
|
|
<div class="flex items-center gap-2"> |
|
|
<span class="text-sm font-medium text-blue-900">Imports (Python)</span> |
|
|
<span class="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded">Run once</span> |
|
|
</div> |
|
|
<button |
|
|
on:click={() => copyToClipboard(importCode)} |
|
|
class="p-1.5 hover:bg-blue-100 rounded transition-colors" |
|
|
title="Copy import code" |
|
|
> |
|
|
<Copy size={14} class="text-blue-600" /> |
|
|
</button> |
|
|
</div> |
|
|
<div class="relative"> |
|
|
<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> |
|
|
</div> |
|
|
</div> |
|
|
{/if} |
|
|
|
|
|
<!-- Show "start using UI" message when no import code or history --> |
|
|
{#if !importCode && codeHistory.length === 0} |
|
|
<div class="bg-white rounded-lg border border-gray-200 p-8 text-center"> |
|
|
<p class="text-gray-500">Start using the UI to see generated code here</p> |
|
|
</div> |
|
|
{/if} |
|
|
|
|
|
<!-- History entries --> |
|
|
{#each codeHistory as entry, i (entry.id)} |
|
|
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm"> |
|
|
|
|
|
<div class="border-b border-gray-200"> |
|
|
<div |
|
|
class="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-100" |
|
|
> |
|
|
<span class="text-sm font-medium text-gray-700">Cell {i + 1}</span> |
|
|
<button |
|
|
on:click={() => copyToClipboard(entry.code)} |
|
|
class="p-1.5 hover:bg-gray-200 rounded transition-colors" |
|
|
title="Copy code" |
|
|
> |
|
|
<Copy size={14} class="text-gray-600" /> |
|
|
</button> |
|
|
</div> |
|
|
<div class="relative"> |
|
|
<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> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Result (audio player) --> |
|
|
{#if entry.result && entry.result.type === 'audio'} |
|
|
<div class="bg-gradient-to-b from-gray-50 to-white p-4"> |
|
|
<div class="bg-white rounded-lg border border-gray-200 p-4 shadow-sm"> |
|
|
<div class="flex items-center justify-between"> |
|
|
<div class="flex items-center gap-3 flex-1"> |
|
|
<button |
|
|
on:click={() => toggleHistoryAudio(entry)} |
|
|
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" |
|
|
> |
|
|
{#if entry.isPlaying} |
|
|
<Pause size={18} /> |
|
|
{:else} |
|
|
<Play size={18} class="ml-0.5" /> |
|
|
{/if} |
|
|
</button> |
|
|
<div class="flex-1"> |
|
|
<div class="text-sm font-medium text-gray-900 truncate"> |
|
|
{entry.result.title || 'Generated Audio'} |
|
|
</div> |
|
|
<div class="text-xs text-gray-500"> |
|
|
Duration: {formatDuration(entry.result.duration || 0)} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="flex items-center gap-1"> |
|
|
<button |
|
|
on:click={() => downloadHistoryAudio(entry.result.url, entry.result.title)} |
|
|
class="p-2 hover:bg-gray-100 rounded-lg transition-colors" |
|
|
title="Download" |
|
|
> |
|
|
<Download size={16} class="text-gray-600" /> |
|
|
</button> |
|
|
<button |
|
|
class="p-2 hover:bg-gray-100 rounded-lg transition-colors" |
|
|
title="Share" |
|
|
> |
|
|
<Share size={16} class="text-gray-600" /> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
<audio |
|
|
bind:this={entry.audioElement} |
|
|
src={entry.result.url} |
|
|
on:ended={() => (entry.isPlaying = false)} |
|
|
class="hidden" |
|
|
/> |
|
|
</div> |
|
|
</div> |
|
|
{/if} |
|
|
</div> |
|
|
{/each} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Copy notification toast --> |
|
|
{#if copyNotification} |
|
|
<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" |
|
|
> |
|
|
{copyNotification} |
|
|
</div> |
|
|
{/if} |
|
|
|
|
|
<style> |
|
|
@keyframes fade-in { |
|
|
from { |
|
|
opacity: 0; |
|
|
transform: translateY(10px); |
|
|
} |
|
|
to { |
|
|
opacity: 1; |
|
|
transform: translateY(0); |
|
|
} |
|
|
} |
|
|
|
|
|
.animate-fade-in { |
|
|
animation: fade-in 0.3s ease-out; |
|
|
} |
|
|
</style> |
|
|
|