💬 Commit message: Update 2026-02-15 05:04:35, 10 files, 374 lines
📁 Files changed: 10 📝 Lines changed: 374 • convert.sh • index-BQdjF_w0.css • index-CKJOpgtQ.js • index-DEHUU-Zz.js • index-Ljwp9hgM.css • index.html • App.vue • DownloadsPanel.vue • ModelCard.vue • app.ts
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Convert a safetensors model to q8_0 GGUF format
|
||||
# Usage: ./scripts/convert.sh <input.safetensors>
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <input.safetensors> [quantization]"
|
||||
echo " quantization: f32, f16, q4_0, q5_0, q8_0 (default: q8_0)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT="$1"
|
||||
QUANT="${2:-q8_0}"
|
||||
|
||||
if [[ ! -f "$INPUT" ]]; then
|
||||
echo "Error: File not found: $INPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Derive output filename: model.safetensors -> model-q8_0.gguf
|
||||
BASENAME=$(basename "$INPUT" .safetensors)
|
||||
DIRNAME=$(dirname "$INPUT")
|
||||
OUTPUT="${DIRNAME}/${BASENAME}-${QUANT}.gguf"
|
||||
|
||||
echo "==> Converting: $INPUT"
|
||||
echo " Output: $OUTPUT"
|
||||
echo " Quantization: $QUANT"
|
||||
echo ""
|
||||
|
||||
sd -M convert -m "$INPUT" -o "$OUTPUT" --type "$QUANT"
|
||||
|
||||
echo ""
|
||||
echo "==> Done: $OUTPUT"
|
||||
ls -lh "$OUTPUT"
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tensors</title>
|
||||
<script type="module" crossorigin src="/assets/index-CKJOpgtQ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BQdjF_w0.css">
|
||||
<script type="module" crossorigin src="/assets/index-DEHUU-Zz.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Ljwp9hgM.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -4,11 +4,13 @@ import { useAppStore } from '@/stores/app'
|
||||
import GenerateView from '@/components/GenerateView.vue'
|
||||
import SearchView from '@/components/SearchView.vue'
|
||||
import GalleryView from '@/components/GalleryView.vue'
|
||||
import DownloadsPanel from '@/components/DownloadsPanel.vue'
|
||||
|
||||
const store = useAppStore()
|
||||
|
||||
onMounted(() => {
|
||||
store.loadModels()
|
||||
store.pollDownloads() // Check for any active downloads on load
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -34,9 +36,31 @@ onMounted(() => {
|
||||
prepend-icon="mdi-image-multiple"
|
||||
title="Gallery"
|
||||
/>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item
|
||||
@click="store.showDownloadsPanel = true"
|
||||
title="Downloads"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-badge
|
||||
v-if="store.hasActiveDownloads"
|
||||
:content="store.activeDownloads.length"
|
||||
color="primary"
|
||||
offset-x="-2"
|
||||
offset-y="-2"
|
||||
>
|
||||
<v-icon>mdi-download</v-icon>
|
||||
</v-badge>
|
||||
<v-icon v-else>mdi-download</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<DownloadsPanel />
|
||||
|
||||
<v-main>
|
||||
<GenerateView v-if="store.currentView === 'generate'" />
|
||||
<SearchView v-else-if="store.currentView === 'search'" />
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const store = useAppStore()
|
||||
|
||||
// Start polling when panel opens or when there are active downloads
|
||||
watch(() => store.showDownloadsPanel, (show) => {
|
||||
if (show) {
|
||||
store.startDownloadPolling()
|
||||
}
|
||||
})
|
||||
|
||||
// Also poll on mount if there might be active downloads
|
||||
onMounted(() => {
|
||||
store.pollDownloads()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Don't stop polling - let it continue for background tracking
|
||||
})
|
||||
|
||||
// Auto-stop polling when no active downloads for a while
|
||||
watch(() => store.hasActiveDownloads, (hasActive) => {
|
||||
if (!hasActive && !store.showDownloadsPanel) {
|
||||
// Give a grace period before stopping
|
||||
setTimeout(() => {
|
||||
if (!store.hasActiveDownloads && !store.showDownloadsPanel) {
|
||||
store.stopDownloadPolling()
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB'
|
||||
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
||||
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
||||
return bytes + ' B'
|
||||
}
|
||||
|
||||
function getStatusColor(status: string): string {
|
||||
switch (status) {
|
||||
case 'completed': return 'success'
|
||||
case 'failed': return 'error'
|
||||
case 'downloading': return 'primary'
|
||||
default: return 'grey'
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusIcon(status: string): string {
|
||||
switch (status) {
|
||||
case 'completed': return 'mdi-check-circle'
|
||||
case 'failed': return 'mdi-alert-circle'
|
||||
case 'downloading': return 'mdi-download'
|
||||
default: return 'mdi-clock-outline'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
v-model="store.showDownloadsPanel"
|
||||
location="right"
|
||||
temporary
|
||||
width="360"
|
||||
>
|
||||
<v-toolbar density="compact" color="surface">
|
||||
<v-toolbar-title class="text-body-1">Downloads</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" size="small" @click="store.showDownloadsPanel = false" />
|
||||
</v-toolbar>
|
||||
|
||||
<v-list v-if="store.downloads.length > 0" density="compact">
|
||||
<v-list-item
|
||||
v-for="dl in store.downloads"
|
||||
:key="dl.id"
|
||||
:class="{ 'bg-surface-light': dl.status === 'downloading' }"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon :color="getStatusColor(dl.status)" class="mr-3">
|
||||
{{ getStatusIcon(dl.status) }}
|
||||
</v-icon>
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="text-body-2 font-weight-medium">
|
||||
{{ dl.model_name || dl.filename }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption">
|
||||
{{ dl.version_name || '' }}
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<!-- Progress bar for active downloads -->
|
||||
<div v-if="dl.status === 'downloading'" class="mt-2">
|
||||
<v-progress-linear
|
||||
:model-value="dl.progress || 0"
|
||||
color="primary"
|
||||
height="6"
|
||||
rounded
|
||||
/>
|
||||
<div class="d-flex justify-space-between text-caption text-grey mt-1">
|
||||
<span>{{ dl.downloaded_str || formatBytes(dl.downloaded || 0) }} / {{ dl.total_str || formatBytes(dl.total || 0) }}</span>
|
||||
<span class="text-primary">{{ dl.speed_str || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Queued status -->
|
||||
<div v-else-if="dl.status === 'queued'" class="mt-2">
|
||||
<v-progress-linear indeterminate color="grey" height="4" rounded />
|
||||
<div class="text-caption text-grey mt-1">Queued...</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed status -->
|
||||
<div v-else-if="dl.status === 'completed'" class="text-caption text-success mt-1">
|
||||
Downloaded to {{ dl.filename }}
|
||||
</div>
|
||||
|
||||
<!-- Failed status -->
|
||||
<div v-else-if="dl.status === 'failed'" class="text-caption text-error mt-1">
|
||||
{{ dl.error || 'Download failed' }}
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<div v-else class="text-center text-grey pa-8">
|
||||
<v-icon size="48" color="grey-darken-1">mdi-download-off</v-icon>
|
||||
<p class="mt-4">No downloads</p>
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
@@ -1,26 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { CivitaiModel } from '@/types'
|
||||
import type { DownloadStatus } from '@/api/client'
|
||||
import * as api from '@/api/client'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const props = defineProps<{
|
||||
model: CivitaiModel
|
||||
}>()
|
||||
|
||||
const store = useAppStore()
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loadingDetails = ref(false)
|
||||
const modelDetails = ref<CivitaiModel | null>(null)
|
||||
|
||||
// Download tracking
|
||||
const activeDownload = ref<DownloadStatus | null>(null)
|
||||
const downloadingVersionId = ref<number | null>(null)
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollInterval) clearInterval(pollInterval)
|
||||
})
|
||||
|
||||
const previewImage = computed(() => {
|
||||
const version = props.model.modelVersions?.[0]
|
||||
return version?.images?.[0]?.url || ''
|
||||
@@ -53,41 +46,12 @@ async function openDetails() {
|
||||
}
|
||||
|
||||
async function downloadVersion(versionId: number) {
|
||||
if (downloadingVersionId.value === versionId) return // Already downloading
|
||||
if (store.isDownloading(versionId)) return // Already downloading
|
||||
if (!confirm(`Download "${props.model.name}" to the server?`)) return
|
||||
|
||||
try {
|
||||
downloadingVersionId.value = versionId
|
||||
const response = await api.downloadModel(undefined, versionId)
|
||||
|
||||
// Start polling for progress
|
||||
const downloadId = response.download_id
|
||||
pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const status = await api.getDownloadStatus(downloadId)
|
||||
activeDownload.value = status
|
||||
|
||||
if (status.status === 'completed' || status.status === 'failed') {
|
||||
if (pollInterval) clearInterval(pollInterval)
|
||||
pollInterval = null
|
||||
|
||||
if (status.status === 'failed') {
|
||||
alert('Download failed: ' + (status.error || 'Unknown error'))
|
||||
}
|
||||
|
||||
// Keep showing completed state briefly, then clear
|
||||
setTimeout(() => {
|
||||
activeDownload.value = null
|
||||
downloadingVersionId.value = null
|
||||
}, 3000)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to get download status:', e)
|
||||
}
|
||||
}, 500)
|
||||
} catch (e: any) {
|
||||
alert('Download failed: ' + e.message)
|
||||
downloadingVersionId.value = null
|
||||
const downloadId = await store.startDownload(versionId)
|
||||
if (!downloadId) {
|
||||
alert('Failed to start download')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -144,26 +108,17 @@ async function downloadVersion(versionId: number) {
|
||||
</span>
|
||||
<v-spacer />
|
||||
<!-- Download progress or button -->
|
||||
<template v-if="downloadingVersionId === model.modelVersions?.[0]?.id && activeDownload">
|
||||
<template v-if="store.isDownloading(model.modelVersions?.[0]?.id || 0)">
|
||||
<div class="download-progress">
|
||||
<v-progress-linear
|
||||
:model-value="activeDownload.progress || 0"
|
||||
:indeterminate="activeDownload.status === 'queued'"
|
||||
:color="activeDownload.status === 'completed' ? 'success' : 'primary'"
|
||||
:model-value="store.getDownloadProgress(model.modelVersions?.[0]?.id || 0)?.progress || 0"
|
||||
:indeterminate="store.getDownloadProgress(model.modelVersions?.[0]?.id || 0)?.status === 'queued'"
|
||||
color="primary"
|
||||
height="6"
|
||||
rounded
|
||||
/>
|
||||
<span class="text-caption text-grey">
|
||||
<template v-if="activeDownload.status === 'completed'">
|
||||
Done!
|
||||
</template>
|
||||
<template v-else-if="activeDownload.status === 'queued'">
|
||||
Queued...
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ activeDownload.downloaded_str }} / {{ activeDownload.total_str }}
|
||||
<span class="text-primary ml-1">{{ activeDownload.speed_str }}</span>
|
||||
</template>
|
||||
{{ store.getDownloadProgress(model.modelVersions?.[0]?.id || 0)?.progress?.toFixed(0) || 0 }}%
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -254,17 +209,17 @@ async function downloadVersion(versionId: number) {
|
||||
<v-list-item-title>{{ version.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ version.baseModel }}</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<template v-if="downloadingVersionId === version.id && activeDownload">
|
||||
<template v-if="store.isDownloading(version.id)">
|
||||
<div class="download-progress-dialog">
|
||||
<v-progress-linear
|
||||
:model-value="activeDownload.progress || 0"
|
||||
:indeterminate="activeDownload.status === 'queued'"
|
||||
:color="activeDownload.status === 'completed' ? 'success' : 'primary'"
|
||||
:model-value="store.getDownloadProgress(version.id)?.progress || 0"
|
||||
:indeterminate="store.getDownloadProgress(version.id)?.status === 'queued'"
|
||||
color="primary"
|
||||
height="6"
|
||||
rounded
|
||||
/>
|
||||
<span class="text-caption text-grey">
|
||||
{{ activeDownload.progress?.toFixed(0) || 0 }}%
|
||||
{{ store.getDownloadProgress(version.id)?.progress?.toFixed(0) || 0 }}%
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Model, LoRA, ResolutionPreset } from '@/types'
|
||||
import type { DownloadStatus } from '@/api/client'
|
||||
import * as api from '@/api/client'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
// Navigation
|
||||
const currentView = ref<'generate' | 'search' | 'gallery'>('generate')
|
||||
|
||||
// Downloads
|
||||
const downloads = ref<DownloadStatus[]>([])
|
||||
const showDownloadsPanel = ref(false)
|
||||
let downloadPollInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// Models
|
||||
const models = ref<Model[]>([])
|
||||
const loras = ref<LoRA[]>([])
|
||||
@@ -97,6 +103,57 @@ export const useAppStore = defineStore('app', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Downloads - computed
|
||||
const activeDownloads = computed(() =>
|
||||
downloads.value.filter(d => d.status === 'downloading' || d.status === 'queued')
|
||||
)
|
||||
const hasActiveDownloads = computed(() => activeDownloads.value.length > 0)
|
||||
|
||||
// Downloads - actions
|
||||
async function pollDownloads() {
|
||||
try {
|
||||
const res = await api.getActiveDownloads()
|
||||
downloads.value = res.downloads || []
|
||||
} catch (e) {
|
||||
console.error('Failed to poll downloads:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function startDownloadPolling() {
|
||||
if (downloadPollInterval) return
|
||||
pollDownloads() // Initial fetch
|
||||
downloadPollInterval = setInterval(pollDownloads, 1000)
|
||||
}
|
||||
|
||||
function stopDownloadPolling() {
|
||||
if (downloadPollInterval) {
|
||||
clearInterval(downloadPollInterval)
|
||||
downloadPollInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
async function startDownload(versionId: number): Promise<string | null> {
|
||||
try {
|
||||
const response = await api.downloadModel(undefined, versionId)
|
||||
startDownloadPolling()
|
||||
showDownloadsPanel.value = true
|
||||
return response.download_id
|
||||
} catch (e: any) {
|
||||
console.error('Failed to start download:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function isDownloading(versionId: number): boolean {
|
||||
return downloads.value.some(
|
||||
d => d.version_id === versionId && (d.status === 'downloading' || d.status === 'queued')
|
||||
)
|
||||
}
|
||||
|
||||
function getDownloadProgress(versionId: number): DownloadStatus | undefined {
|
||||
return downloads.value.find(d => d.version_id === versionId)
|
||||
}
|
||||
|
||||
async function switchModel(modelPath: string) {
|
||||
if (modelPath === activeModel.value) return
|
||||
|
||||
@@ -169,8 +226,20 @@ export const useAppStore = defineStore('app', () => {
|
||||
switchMessage,
|
||||
switchError,
|
||||
|
||||
// Downloads
|
||||
downloads,
|
||||
activeDownloads,
|
||||
hasActiveDownloads,
|
||||
showDownloadsPanel,
|
||||
|
||||
// Actions
|
||||
loadModels,
|
||||
switchModel,
|
||||
startDownload,
|
||||
pollDownloads,
|
||||
startDownloadPolling,
|
||||
stopDownloadPolling,
|
||||
isDownloading,
|
||||
getDownloadProgress,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user