|
<script lang="ts"> |
|
import type { GradioClient } from '$lib/types'; |
|
import { |
|
initializeTrainerScanProgress, |
|
getNextPendingImage, |
|
markImageProcessingCompleted, |
|
markImageProcessingFailed, |
|
getScanningStats |
|
} from '$lib/db/trainerScanning'; |
|
import PicletGenerator from '$lib/components/PicletGenerator/PicletGenerator.svelte'; |
|
|
|
interface Props { |
|
joyCaptionClient: GradioClient; |
|
fluxClient: GradioClient; |
|
commandClient: GradioClient; |
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
let { joyCaptionClient, fluxClient, commandClient }: Props = $props(); |
|
|
|
|
|
let scanState = $state({ |
|
isScanning: false, |
|
currentImage: null as string | null, |
|
currentTrainer: null as string | null, |
|
progress: { |
|
total: 0, |
|
completed: 0, |
|
failed: 0, |
|
pending: 0 |
|
}, |
|
error: null as string | null |
|
}); |
|
|
|
|
|
let currentImagePromise: { |
|
resolve: (value: any) => void; |
|
reject: (error: any) => void; |
|
} | null = null; |
|
|
|
let showDetails = $state(false); |
|
let isInitializing = $state(false); |
|
let shouldStop = $state(false); |
|
|
|
|
|
let picletGenerator: any; |
|
|
|
|
|
$effect(() => { |
|
if (joyCaptionClient && fluxClient && commandClient) { |
|
loadInitialState(); |
|
} |
|
}); |
|
|
|
async function loadInitialState() { |
|
try { |
|
isInitializing = true; |
|
|
|
|
|
const response = await fetch('/trainer_image_paths.txt'); |
|
if (!response.ok) { |
|
throw new Error(`Failed to fetch trainer_image_paths.txt: ${response.statusText}`); |
|
} |
|
|
|
const content = await response.text(); |
|
const imagePaths = content.trim().split('\n') |
|
.map(path => typeof path === 'string' ? path.trim() : '') |
|
.filter(path => path.length > 0); |
|
|
|
console.log(`Loaded ${imagePaths.length} trainer image paths`); |
|
|
|
await initializeTrainerScanProgress(imagePaths); |
|
await updateProgress(); |
|
} catch (error) { |
|
console.error('Failed to initialize scanner:', error); |
|
scanState.error = error instanceof Error ? error.message : 'Failed to initialize'; |
|
} finally { |
|
isInitializing = false; |
|
} |
|
} |
|
|
|
async function updateProgress() { |
|
const stats = await getScanningStats(); |
|
scanState.progress = { |
|
total: stats.total, |
|
completed: stats.completed, |
|
failed: stats.failed, |
|
pending: stats.pending |
|
}; |
|
} |
|
|
|
async function startScanning() { |
|
if (scanState.isScanning) return; |
|
|
|
scanState.isScanning = true; |
|
scanState.error = null; |
|
shouldStop = false; |
|
|
|
try { |
|
await processTrainerImages(); |
|
} catch (error) { |
|
console.error('Scanning error:', error); |
|
scanState.error = error instanceof Error ? error.message : 'Unknown error'; |
|
} finally { |
|
scanState.isScanning = false; |
|
scanState.currentImage = null; |
|
scanState.currentTrainer = null; |
|
} |
|
} |
|
|
|
function stopScanning() { |
|
shouldStop = true; |
|
} |
|
|
|
async function processTrainerImages() { |
|
while (!shouldStop) { |
|
const nextImage = await getNextPendingImage(); |
|
|
|
if (!nextImage) { |
|
|
|
break; |
|
} |
|
|
|
scanState.currentImage = nextImage.imagePath; |
|
scanState.currentTrainer = nextImage.trainerName; |
|
|
|
try { |
|
|
|
const imageFile = await fetchRemoteImage(nextImage.remoteUrl, nextImage.imagePath); |
|
|
|
|
|
const imageProcessingPromise = new Promise<void>((resolve, reject) => { |
|
currentImagePromise = { resolve, reject }; |
|
}); |
|
|
|
|
|
if (picletGenerator) { |
|
picletGenerator.queueTrainerImage(imageFile, nextImage.imagePath); |
|
} |
|
|
|
|
|
console.log(`🔧 DEBUG: Waiting for completion of ${nextImage.imagePath}...`); |
|
await imageProcessingPromise; |
|
console.log(`✅ DEBUG: Completed processing of ${nextImage.imagePath}, moving to next image`); |
|
|
|
} catch (error) { |
|
console.error(`Failed to process ${nextImage.imagePath}:`, error); |
|
await markImageProcessingFailed( |
|
nextImage.imagePath, |
|
error instanceof Error ? error.message : 'Unknown error' |
|
); |
|
|
|
|
|
if (currentImagePromise) { |
|
currentImagePromise.reject(error); |
|
currentImagePromise = null; |
|
} |
|
} |
|
|
|
await updateProgress(); |
|
} |
|
} |
|
|
|
async function fetchRemoteImage(remoteUrl: string, originalPath: string): Promise<File> { |
|
const response = await fetch(remoteUrl); |
|
if (!response.ok) { |
|
throw new Error(`Failed to fetch ${remoteUrl}: ${response.statusText}`); |
|
} |
|
|
|
const blob = await response.blob(); |
|
const fileName = originalPath.split('/').pop() || 'trainer_image.jpg'; |
|
|
|
return new File([blob], fileName, { type: blob.type }); |
|
} |
|
|
|
async function onTrainerImageCompleted(imagePath: string, picletId: number) { |
|
console.log(`✅ Trainer image completed: ${imagePath} -> Piclet ID: ${picletId}`); |
|
|
|
try { |
|
await markImageProcessingCompleted(imagePath, picletId); |
|
} catch (error) { |
|
console.error(`❌ Failed to mark ${imagePath} as completed:`, error); |
|
} |
|
|
|
await updateProgress(); |
|
|
|
|
|
if (currentImagePromise) { |
|
currentImagePromise.resolve(undefined); |
|
currentImagePromise = null; |
|
} |
|
} |
|
|
|
async function onTrainerImageFailed(imagePath: string, error: string) { |
|
console.error(`❌ Trainer image failed: ${imagePath} -> ${error}`); |
|
await markImageProcessingFailed(imagePath, error); |
|
await updateProgress(); |
|
|
|
|
|
if (currentImagePromise) { |
|
currentImagePromise.resolve(undefined); |
|
currentImagePromise = null; |
|
} |
|
} |
|
|
|
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 ''; |
|
|
|
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> |
|
</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> |
|
|
|
<!-- Hidden PicletGenerator for trainer mode processing --> |
|
<div style="display: none;"> |
|
<PicletGenerator |
|
bind:this={picletGenerator} |
|
{joyCaptionClient} |
|
{fluxClient} |
|
{commandClient} |
|
isTrainerMode={true} |
|
onTrainerImageCompleted={onTrainerImageCompleted} |
|
onTrainerImageFailed={onTrainerImageFailed} |
|
/> |
|
</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 { |
|
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); |
|
} |
|
|
|
|
|
.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> |