fix(vision): use --clear flag for venv creation and add preflight check
- Use Python's --clear flag to handle existing venv directories atomically - Disable "Start Training" button until hardware setup succeeds - Show "Setup Failed" state on button when hardware detection fails - Add "Retry Setup" button when setup fails 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -117,14 +117,8 @@ async function createVenv(): Promise<SetupResult> {
|
||||
console.log(`[vision-training] Creating venv with ${basePython}...`)
|
||||
|
||||
try {
|
||||
// Remove incomplete venv if it exists
|
||||
if (fs.existsSync(VENV_DIR)) {
|
||||
console.log('[vision-training] Removing incomplete venv...')
|
||||
fs.rmSync(VENV_DIR, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
// Create venv
|
||||
await execAsync(`"${basePython}" -m venv "${VENV_DIR}"`, { timeout: 60000 })
|
||||
// Create venv (--clear removes existing incomplete venv)
|
||||
await execAsync(`"${basePython}" -m venv --clear "${VENV_DIR}"`, { timeout: 60000 })
|
||||
|
||||
// Upgrade pip
|
||||
await execAsync(`"${TRAINING_PYTHON}" -m pip install --upgrade pip`, {
|
||||
|
||||
@@ -74,28 +74,32 @@ export default function TrainModelPage() {
|
||||
const eventSourceRef = useRef<EventSource | null>(null)
|
||||
const logsEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Fetch hardware info (also used for retry)
|
||||
const fetchHardware = useCallback(async () => {
|
||||
setHardwareLoading(true)
|
||||
setHardwareInfo(null)
|
||||
try {
|
||||
const response = await fetch('/api/vision-training/hardware')
|
||||
const data = await response.json()
|
||||
setHardwareInfo(data)
|
||||
} catch {
|
||||
setHardwareInfo({
|
||||
available: false,
|
||||
device: 'unknown',
|
||||
deviceName: 'Failed to detect',
|
||||
deviceType: 'unknown',
|
||||
details: {},
|
||||
error: 'Failed to fetch hardware info',
|
||||
})
|
||||
} finally {
|
||||
setHardwareLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Fetch hardware info on mount
|
||||
useEffect(() => {
|
||||
async function fetchHardware() {
|
||||
try {
|
||||
const response = await fetch('/api/vision-training/hardware')
|
||||
const data = await response.json()
|
||||
setHardwareInfo(data)
|
||||
} catch {
|
||||
setHardwareInfo({
|
||||
available: false,
|
||||
device: 'unknown',
|
||||
deviceName: 'Failed to detect',
|
||||
deviceType: 'unknown',
|
||||
details: {},
|
||||
error: 'Failed to fetch hardware info',
|
||||
})
|
||||
} finally {
|
||||
setHardwareLoading(false)
|
||||
}
|
||||
}
|
||||
fetchHardware()
|
||||
}, [])
|
||||
}, [fetchHardware])
|
||||
|
||||
// Auto-scroll logs
|
||||
useEffect(() => {
|
||||
@@ -507,13 +511,36 @@ export default function TrainModelPage() {
|
||||
|
||||
{/* Error details */}
|
||||
{hardwareInfo?.error && (
|
||||
<div className={css({ mt: 2, fontSize: 'sm', color: 'red.300' })}>
|
||||
{hardwareInfo.error}
|
||||
<div className={css({ mt: 2 })}>
|
||||
<p className={css({ fontSize: 'sm', color: 'red.300' })}>
|
||||
{hardwareInfo.error}
|
||||
</p>
|
||||
{hardwareInfo.hint && (
|
||||
<span className={css({ display: 'block', color: 'gray.400', mt: 1 })}>
|
||||
<p className={css({ fontSize: 'sm', color: 'gray.400', mt: 1 })}>
|
||||
Hint: {hardwareInfo.hint}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={fetchHardware}
|
||||
disabled={hardwareLoading}
|
||||
className={css({
|
||||
mt: 2,
|
||||
px: 3,
|
||||
py: 1,
|
||||
bg: 'blue.600',
|
||||
color: 'white',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
borderRadius: 'md',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'blue.700' },
|
||||
_disabled: { opacity: 0.6, cursor: 'not-allowed' },
|
||||
})}
|
||||
>
|
||||
{hardwareLoading ? 'Retrying...' : 'Retry Setup'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -536,6 +563,7 @@ export default function TrainModelPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={startTraining}
|
||||
disabled={hardwareLoading || !!hardwareInfo?.error}
|
||||
className={css({
|
||||
flex: 1,
|
||||
px: 6,
|
||||
@@ -548,9 +576,14 @@ export default function TrainModelPage() {
|
||||
cursor: 'pointer',
|
||||
fontSize: 'lg',
|
||||
_hover: { bg: 'green.700' },
|
||||
_disabled: {
|
||||
bg: 'gray.600',
|
||||
cursor: 'not-allowed',
|
||||
opacity: 0.6,
|
||||
},
|
||||
})}
|
||||
>
|
||||
Start Training
|
||||
{hardwareLoading ? 'Setting up...' : hardwareInfo?.error ? 'Setup Failed' : 'Start Training'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user