piclets / src /lib /components /AutoTrainerScanner /AutoTrainerScanner.svelte
Fraser's picture
t s
5435413
raw
history blame
11.7 kB
<script lang="ts">
import type { GradioClient } from '$lib/types';
import { TrainerScanService, type TrainerScanState } from '$lib/services/trainerScanService';
import { resetFailedScans } from '$lib/db/trainerScanning';
interface Props {
joyCaptionClient: GradioClient;
zephyrClient: GradioClient;
fluxClient: GradioClient;
}
let { joyCaptionClient, zephyrClient, fluxClient }: Props = $props();
// Scanner service and state
let scanService: TrainerScanService | null = null;
let scanState: TrainerScanState = $state({
isScanning: false,
currentImage: null,
currentTrainer: null,
progress: {
total: 0,
completed: 0,
failed: 0,
pending: 0
},
error: null
});
let showDetails = $state(false);
let isInitializing = $state(false);
// Initialize scan service when clients are available
$effect(() => {
if (joyCaptionClient && zephyrClient && fluxClient && !scanService) {
scanService = new TrainerScanService(joyCaptionClient, zephyrClient, fluxClient);
// Subscribe to state changes
scanService.onStateChange((newState) => {
scanState = newState;
});
// Load initial state
loadInitialState();
}
});
async function loadInitialState() {
if (!scanService) return;
try {
isInitializing = true;
await scanService.initializeFromFile();
} catch (error) {
console.error('Failed to initialize scanner:', error);
scanState.error = error instanceof Error ? error.message : 'Failed to initialize';
} finally {
isInitializing = false;
}
}
async function startScanning() {
if (!scanService) return;
try {
await scanService.startScanning();
} catch (error) {
console.error('Failed to start scanning:', error);
scanState.error = error instanceof Error ? error.message : 'Failed to start scanning';
}
}
function stopScanning() {
if (!scanService) return;
scanService.stopScanning();
}
async function retryFailedScans() {
try {
const resetCount = await resetFailedScans();
console.log(`Reset ${resetCount} failed scans to pending`);
// Refresh state
if (scanService) {
await scanService.initializeFromFile();
}
} catch (error) {
console.error('Failed to retry failed scans:', error);
scanState.error = error instanceof Error ? error.message : 'Failed to retry failed scans';
}
}
function formatImageName(imagePath: string | null): string {
if (!imagePath) return '';
const parts = imagePath.split('/');
return parts[parts.length - 1] || '';
}
function formatTrainerName(trainerName: string | null): string {
if (!trainerName) return '';
// Convert "001_Willow_Snap" to "Willow Snap"
return trainerName.split('_').slice(1).join(' ');
}
function getProgressPercent(): number {
const { total, completed } = scanState.progress;
return total > 0 ? Math.round((completed / total) * 100) : 0;
}
</script>
<div class="auto-trainer-scanner">
<div class="scanner-header">
<div class="title-section">
<h3>🤖 Auto Trainer Scanner</h3>
<button
class="details-toggle"
onclick={() => showDetails = !showDetails}
>
{showDetails ? '▼' : '▶'} Details
</button>
</div>
{#if scanState.progress.total > 0}
<div class="progress-summary">
<div class="progress-bar">
<div
class="progress-fill"
style="width: {getProgressPercent()}%"
></div>
</div>
<span class="progress-text">
{scanState.progress.completed} / {scanState.progress.total} ({getProgressPercent()}%)
</span>
</div>
{/if}
</div>
{#if showDetails}
<div class="scanner-details">
{#if isInitializing}
<div class="status-message">
<div class="spinner"></div>
<span>Initializing scanner...</span>
</div>
{:else if scanState.isScanning}
<div class="scanning-status">
<div class="current-processing">
<div class="spinner"></div>
<div class="processing-info">
<div class="current-trainer">
Processing: <strong>{formatTrainerName(scanState.currentTrainer)}</strong>
</div>
<div class="current-image">
{formatImageName(scanState.currentImage)}
</div>
</div>
</div>
<button class="stop-button" onclick={stopScanning}>
⏹️ Stop Scanning
</button>
</div>
{:else}
<div class="scanner-controls">
<button
class="start-button"
onclick={startScanning}
disabled={scanState.progress.pending === 0}
>
▶️ Start Auto Scan
</button>
{#if scanState.progress.failed > 0}
<button class="retry-button" onclick={retryFailedScans}>
🔄 Retry Failed ({scanState.progress.failed})
</button>
{/if}
</div>
{/if}
{#if scanState.progress.total > 0}
<div class="progress-details">
<div class="progress-stats">
<div class="stat">
<span class="stat-label">Total:</span>
<span class="stat-value">{scanState.progress.total}</span>
</div>
<div class="stat completed">
<span class="stat-label">Completed:</span>
<span class="stat-value">{scanState.progress.completed}</span>
</div>
<div class="stat pending">
<span class="stat-label">Pending:</span>
<span class="stat-value">{scanState.progress.pending}</span>
</div>
{#if scanState.progress.failed > 0}
<div class="stat failed">
<span class="stat-label">Failed:</span>
<span class="stat-value">{scanState.progress.failed}</span>
</div>
{/if}
</div>
</div>
{/if}
{#if scanState.error}
<div class="error-message">
<strong>Error:</strong> {scanState.error}
</div>
{/if}
<div class="scanner-info">
<p>
This will automatically process trainer images from the HuggingFace dataset,
converting them into unique Piclets. The scanner will resume from where it left off
if interrupted.
</p>
</div>
</div>
{/if}
</div>
<style>
.auto-trainer-scanner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 1rem;
margin-bottom: 1rem;
color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.scanner-header {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.title-section {
display: flex;
align-items: center;
justify-content: space-between;
}
.title-section h3 {
margin: 0;
font-size: 1.1rem;
}
.details-toggle {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
padding: 0.3rem 0.6rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s;
}
.details-toggle:hover {
background: rgba(255, 255, 255, 0.3);
}
.progress-summary {
display: flex;
align-items: center;
gap: 1rem;
}
.progress-bar {
flex: 1;
height: 8px;
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
transition: width 0.3s ease;
}
.progress-text {
font-size: 0.9rem;
font-weight: 500;
white-space: nowrap;
}
.scanner-details {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.status-message {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.8rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
margin-bottom: 1rem;
}
.scanning-status {
display: flex;
flex-direction: column;
gap: 1rem;
}
.current-processing {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
}
.processing-info {
flex: 1;
}
.current-trainer {
font-size: 1rem;
margin-bottom: 0.3rem;
}
.current-image {
font-size: 0.9rem;
opacity: 0.8;
}
.scanner-controls {
display: flex;
gap: 0.8rem;
margin-bottom: 1rem;
}
.start-button, .stop-button, .retry-button {
padding: 0.8rem 1.2rem;
border: none;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.start-button {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.start-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(79, 172, 254, 0.3);
}
.start-button:disabled {
background: rgba(255, 255, 255, 0.3);
cursor: not-allowed;
opacity: 0.6;
}
.stop-button {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
color: white;
}
.stop-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(255, 107, 107, 0.3);
}
.retry-button {
background: linear-gradient(135deg, #ffa726 0%, #ff7043 100%);
color: white;
}
.retry-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(255, 167, 38, 0.3);
}
.progress-details {
margin-bottom: 1rem;
}
.progress-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.8rem;
}
.stat {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.6rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
border-left: 3px solid rgba(255, 255, 255, 0.5);
}
.stat.completed {
border-left-color: #4caf50;
}
.stat.pending {
border-left-color: #ff9800;
}
.stat.failed {
border-left-color: #f44336;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.9;
}
.stat-value {
font-weight: 600;
font-size: 1rem;
}
.error-message {
background: rgba(244, 67, 54, 0.2);
border: 1px solid rgba(244, 67, 54, 0.4);
border-radius: 8px;
padding: 0.8rem;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.scanner-info {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 0.8rem;
font-size: 0.9rem;
line-height: 1.4;
}
.scanner-info p {
margin: 0;
opacity: 0.9;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.progress-summary {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.current-processing {
flex-direction: column;
align-items: flex-start;
text-align: left;
}
.scanner-controls {
flex-direction: column;
}
.progress-stats {
grid-template-columns: 1fr;
}
}
</style>