GitHub Action
Sync from GitHub: 3f27e5802e9ea1f29a6e07e9d09b3bd2ef3e27ec
a4093da
<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">
<!-- Header -->
<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>
<!-- Toggle and Copy All button row -->
<div class="flex items-center justify-between mt-4">
<!-- API/Local Mode Toggle -->
<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">
<!-- Setup Section - Always shown -->
{#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">
<!-- Code cell -->
<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>