fix(progress): separate model warmup state
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import type { Job, AudioMode } from '$lib/types.js';
|
||||
import { getDisplayJobProgress, getJobStatusLabel } from '$lib/job-progress.js';
|
||||
import SourceIcon from '$lib/components/SourceIcon.svelte';
|
||||
import Waveform from '$lib/components/Waveform.svelte';
|
||||
import RecordButton from '$lib/components/RecordButton.svelte';
|
||||
@@ -95,8 +96,7 @@
|
||||
const parts: string[] = [];
|
||||
if (job.source && !job.source.startsWith('http')) parts.push(job.source.split('/').pop() ?? '');
|
||||
if (job.audioMode) parts.push(job.audioMode);
|
||||
if (job.status === 'done') parts.push('done');
|
||||
else parts.push(job.status);
|
||||
parts.push(getJobStatusLabel(job.status).toLowerCase());
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
@@ -271,7 +271,9 @@
|
||||
<div class="recent-meta mono">{jobMeta(job)}</div>
|
||||
</div>
|
||||
{#if job.status !== 'done' && job.status !== 'failed' && job.status !== 'cancelled'}
|
||||
<div class="recent-progress mono" style="color: {ACCENT}">{job.progress}%</div>
|
||||
<div class="recent-progress mono" style="color: {ACCENT}">
|
||||
{getDisplayJobProgress(job, { hasTranscript: Boolean(job.segmentsJson) })}%
|
||||
</div>
|
||||
{/if}
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" style="color: var(--text-dim); flex-shrink:0">
|
||||
<path d="M5 3l4 4-4 4" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function GET({ params }) {
|
||||
return json(job);
|
||||
}
|
||||
|
||||
const ACTIVE = new Set(['pending', 'downloading', 'preparing', 'transcribing', 'processing']);
|
||||
const ACTIVE = new Set(['pending', 'downloading', 'preparing', 'warming_model', 'transcribing', 'processing']);
|
||||
|
||||
export async function DELETE({ params }) {
|
||||
const job = getJob(params.id);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import type { Job } from '$lib/types.js';
|
||||
import { getDisplayJobProgress, getJobStatusColor, getJobStatusLabel, isTerminalJobStatus } from '$lib/job-progress.js';
|
||||
import SourceIcon from '$lib/components/SourceIcon.svelte';
|
||||
import Waveform from '$lib/components/Waveform.svelte';
|
||||
import { accent } from '$lib/accent.js';
|
||||
@@ -10,27 +11,6 @@
|
||||
let jobs = $state<Job[]>([]);
|
||||
let loading = $state(true);
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
done: '#cdf24e',
|
||||
failed: '#ff6b6b',
|
||||
cancelled: 'rgba(232,233,236,0.3)',
|
||||
transcribing: '#80c7f7',
|
||||
preparing: '#fbc94b',
|
||||
downloading: '#a78bfa',
|
||||
pending: 'rgba(232,233,236,0.4)'
|
||||
};
|
||||
|
||||
const statusLabel: Record<string, string> = {
|
||||
pending: 'Pending',
|
||||
downloading: 'Downloading',
|
||||
preparing: 'Preparing',
|
||||
transcribing: 'Transcribing',
|
||||
processing: 'Processing',
|
||||
done: 'Done',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled'
|
||||
};
|
||||
|
||||
function jobKind(job: Job): 'youtube' | 'audio' | 'video' | 'file' {
|
||||
const s = job.source ?? '';
|
||||
if (s.includes('youtube') || s.includes('youtu.be')) return 'youtube';
|
||||
@@ -108,8 +88,8 @@
|
||||
<div class="job-info">
|
||||
<div class="job-name">{job.title || job.source}</div>
|
||||
<div class="job-meta mono">
|
||||
<span style="color: {statusColor[job.status] ?? 'rgba(232,233,236,0.5)'}">
|
||||
{statusLabel[job.status] ?? job.status}
|
||||
<span style="color: {getJobStatusColor(job.status)}">
|
||||
{getJobStatusLabel(job.status)}
|
||||
</span>
|
||||
{#if job.createdAt}
|
||||
<span>·</span>
|
||||
@@ -122,14 +102,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !['done', 'failed', 'cancelled'].includes(job.status)}
|
||||
{#if !isTerminalJobStatus(job.status)}
|
||||
<div class="job-wave">
|
||||
<Waveform bars={40} progress={job.progress} accent={ACCENT} height={28} pattern="medium" />
|
||||
<Waveform
|
||||
bars={40}
|
||||
progress={getDisplayJobProgress(job, { hasTranscript: Boolean(job.segmentsJson) })}
|
||||
accent={ACCENT}
|
||||
height={28}
|
||||
pattern="medium"
|
||||
/>
|
||||
</div>
|
||||
{:else if job.status === 'done'}
|
||||
<div class="job-pct mono" style="color: {ACCENT}">{job.progress}%</div>
|
||||
<div class="job-pct mono" style="color: {ACCENT}">
|
||||
{getDisplayJobProgress(job, { hasTranscript: Boolean(job.segmentsJson) })}%
|
||||
</div>
|
||||
{:else}
|
||||
<div class="job-pct mono" style="color: {statusColor[job.status]}">{job.status}</div>
|
||||
<div class="job-pct mono" style="color: {getJobStatusColor(job.status)}">
|
||||
{getJobStatusLabel(job.status)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Row actions -->
|
||||
@@ -142,7 +132,7 @@
|
||||
title="Retry"
|
||||
>↺</button>
|
||||
{/if}
|
||||
{#if ['done', 'failed', 'cancelled'].includes(job.status)}
|
||||
{#if isTerminalJobStatus(job.status)}
|
||||
<button
|
||||
class="row-btn danger"
|
||||
onclick={(e) => deleteJob(e, job)}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import type { Job, Segment } from '$lib/types.js';
|
||||
import { getDisplayJobProgress, getJobStatusLabel, isTerminalJobStatus } from '$lib/job-progress.js';
|
||||
import SourceIcon from '$lib/components/SourceIcon.svelte';
|
||||
import Waveform from '$lib/components/Waveform.svelte';
|
||||
import { accent } from '$lib/accent.js';
|
||||
@@ -16,17 +17,6 @@
|
||||
let modelWarming = $state<{ state: string; retryAfterSecs: number } | null>(null);
|
||||
let eventSource: EventSource | null = null;
|
||||
|
||||
const statusLabel: Record<string, string> = {
|
||||
pending: 'Pending',
|
||||
downloading: 'Downloading…',
|
||||
preparing: 'Preparing audio…',
|
||||
transcribing: 'Transcribing…',
|
||||
processing: 'Post-processing…',
|
||||
done: 'Done',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled'
|
||||
};
|
||||
|
||||
// Pipeline stages derived from job status
|
||||
const pipelineStages = $derived.by(() => {
|
||||
const status = job?.status ?? 'pending';
|
||||
@@ -34,16 +24,26 @@
|
||||
{ k: 'fetch', label: 'Fetch source' },
|
||||
{ k: 'extract', label: 'Extract audio track' },
|
||||
{ k: 'process', label: `Audio processing · ${job?.audioMode ?? 'auto'}` },
|
||||
{ k: 'transcribe', label: 'Transcribing' },
|
||||
{ k: 'transcribe', label: status === 'warming_model' ? 'Loading model' : 'Transcribing' },
|
||||
{ k: 'finalize', label: 'Format & save' }
|
||||
];
|
||||
const order = ['pending', 'downloading', 'preparing', 'transcribing', 'processing', 'done'];
|
||||
const idx = order.indexOf(status);
|
||||
const stageIndex = {
|
||||
pending: 0,
|
||||
downloading: 1,
|
||||
preparing: 2,
|
||||
warming_model: 3,
|
||||
transcribing: 3,
|
||||
processing: 4,
|
||||
done: 5,
|
||||
failed: -1,
|
||||
cancelled: -1
|
||||
}[status];
|
||||
|
||||
return stages.map((s, i) => ({
|
||||
...s,
|
||||
done: i < idx - 1 || status === 'done',
|
||||
active: i === idx - 1 && status !== 'done' && status !== 'failed',
|
||||
pending: i > idx - 1 && status !== 'done'
|
||||
done: status === 'done' || i + 1 < stageIndex,
|
||||
active: i + 1 === stageIndex && !isTerminalJobStatus(status),
|
||||
pending: status !== 'done' && i + 1 > stageIndex
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
onMount(async () => {
|
||||
await loadJob();
|
||||
if (job && !['done', 'failed', 'cancelled'].includes(job.status)) {
|
||||
if (job && !isTerminalJobStatus(job.status)) {
|
||||
openStream();
|
||||
}
|
||||
});
|
||||
@@ -71,6 +71,7 @@
|
||||
return;
|
||||
}
|
||||
job = await res.json();
|
||||
segments = [];
|
||||
if (job?.segmentsJson) {
|
||||
try {
|
||||
segments = JSON.parse(job.segmentsJson);
|
||||
@@ -89,7 +90,11 @@
|
||||
if (job) job = { ...job, progress: data.progress ?? job.progress, status: 'transcribing' };
|
||||
} else if (data.type === 'model_warming') {
|
||||
modelWarming = { state: data.state ?? 'loading', retryAfterSecs: data.retryAfterSecs ?? 30 };
|
||||
chunkInfo = { chunk: 0, total: 0 };
|
||||
if (job) job = { ...job, status: 'warming_model', progress: data.progress ?? job.progress };
|
||||
} else if (data.type === 'status') {
|
||||
if (data.status !== 'warming_model') modelWarming = null;
|
||||
if (data.status !== 'transcribing') chunkInfo = { chunk: 0, total: 0 };
|
||||
if (job) job = { ...job, status: data.status, progress: data.progress ?? job.progress };
|
||||
} else if (data.type === 'done') {
|
||||
modelWarming = null;
|
||||
@@ -113,8 +118,21 @@
|
||||
}
|
||||
|
||||
const formats = ['srt', 'txt', 'md', 'json'] as const;
|
||||
const isActive = $derived(!job || !['done', 'failed', 'cancelled'].includes(job.status));
|
||||
const isTerminal = $derived(job !== null && ['done', 'failed', 'cancelled'].includes(job.status));
|
||||
const hasTranscript = $derived((job?.segmentsJson ? true : false) || segments.length > 0);
|
||||
const displayProgress = $derived(job ? getDisplayJobProgress(job, { hasTranscript }) : 0);
|
||||
const progressStatusLabel = $derived.by(() => {
|
||||
if (!job) return 'Pending';
|
||||
if (job.status === 'warming_model') {
|
||||
const state = modelWarming?.state?.replace(/_/g, ' ');
|
||||
return state ? `Loading model (${state})…` : 'Loading model…';
|
||||
}
|
||||
if (job.status === 'preparing') return 'Preparing audio…';
|
||||
if (job.status === 'processing') return 'Saving transcript…';
|
||||
if (job.status === 'transcribing') return 'Transcribing…';
|
||||
return job.status === 'done' ? 'Done' : `${getJobStatusLabel(job.status)}…`;
|
||||
});
|
||||
const isActive = $derived(!job || !isTerminalJobStatus(job.status));
|
||||
const isTerminal = $derived(job !== null && isTerminalJobStatus(job.status));
|
||||
const canRetry = $derived(
|
||||
job !== null &&
|
||||
['failed', 'cancelled'].includes(job.status) &&
|
||||
@@ -197,22 +215,22 @@
|
||||
<div class="progress-card glass">
|
||||
<!-- Waveform coloured by progress -->
|
||||
<div class="progress-wave">
|
||||
<Waveform
|
||||
bars={140}
|
||||
progress={job.progress}
|
||||
accent={ACCENT}
|
||||
height={80}
|
||||
pattern="default"
|
||||
<Waveform
|
||||
bars={140}
|
||||
progress={displayProgress}
|
||||
accent={ACCENT}
|
||||
height={80}
|
||||
pattern="default"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="progress-footer">
|
||||
<div class="progress-left">
|
||||
<span class="progress-pct mono">
|
||||
{job.progress}<span style="color: var(--text-dim); font-weight: 400">%</span>
|
||||
</span>
|
||||
<span class="progress-status">{statusLabel[job.status] ?? job.status}</span>
|
||||
</div>
|
||||
<div class="progress-footer">
|
||||
<div class="progress-left">
|
||||
<span class="progress-pct mono">
|
||||
{displayProgress}<span style="color: var(--text-dim); font-weight: 400">%</span>
|
||||
</span>
|
||||
<span class="progress-status">{progressStatusLabel}</span>
|
||||
</div>
|
||||
{#if chunkInfo.total > 0}
|
||||
<span class="progress-chunks mono">
|
||||
chunk {chunkInfo.chunk} / {chunkInfo.total}
|
||||
@@ -233,7 +251,7 @@
|
||||
<div class="progress-bar-track">
|
||||
<div
|
||||
class="progress-bar-fill"
|
||||
style="width: {job.progress}%; background: {ACCENT}; box-shadow: 0 0 12px {ACCENT}80;"
|
||||
style="width: {displayProgress}%; background: {ACCENT}; box-shadow: 0 0 12px {ACCENT}80;"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -275,7 +293,7 @@
|
||||
{@html stage.label}
|
||||
</span>
|
||||
{#if stage.active}
|
||||
<span class="mono" style="font-size: 11.5px; color: {ACCENT}">{job.progress}%</span>
|
||||
<span class="mono" style="font-size: 11.5px; color: {ACCENT}">{displayProgress}%</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -690,4 +708,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user