fix: increase VideoGen timeout to 6 minutes for GPU cold starts

Video generation on RunPod can take significant time:
- GPU cold start: 30-120 seconds
- Model loading: 30-60 seconds
- Generation: 60-180 seconds

Increased polling timeout from 4 to 6 minutes and updated UI
to set proper expectations for users.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-11-30 18:51:41 -08:00
parent 1b234d9dda
commit e0f8107e1d
4 changed files with 50 additions and 25 deletions

View File

@ -620,11 +620,12 @@ export function sanitizeRecord(record: any): TLRecord {
sanitized.meta = { ...sanitized.meta }
}
// CRITICAL: IndexKey must follow tldraw's fractional indexing format
// Valid format: starts with 'a' followed by digits, optionally followed by uppercase letters
// Examples: "a1", "a2", "a10", "a1V" (fractional between a1 and a2)
// Invalid: "c1", "b1", "z999" (must start with 'a')
if (!sanitized.index || typeof sanitized.index !== 'string' || !/^a\d+[A-Z]*$/.test(sanitized.index)) {
sanitized.index = 'a1'
// Valid format: starts with 'a' followed by digits, optionally followed by alphanumeric jitter
// Examples: "a1", "a2", "a10", "a1V", "a24sT", "a1V4rr" (fractional between a1 and a2)
// Invalid: "c1", "b1", "z999" (old format - not valid fractional indices)
if (!isValidIndexKey(sanitized.index)) {
console.warn(`⚠️ Invalid index "${sanitized.index}" for shape ${sanitized.id}, resetting to 'a1'`)
sanitized.index = 'a1' as IndexKey
}
if (!sanitized.parentId) sanitized.parentId = 'page:page'
if (!sanitized.props || typeof sanitized.props !== 'object') sanitized.props = {}

View File

@ -10,36 +10,41 @@ import { getDocumentId, saveDocumentId } from "./documentIdMapping"
/**
* Validate if an index is a valid tldraw fractional index
* Valid indices: "a0", "a1", "a1V", "a2", "Zz", etc.
* Invalid indices: "b1", "c2", or any simple letter+number that isn't "a" followed by proper format
* Valid indices: "a0", "a1", "a1V", "a24sT", "a1V4rr", "Zz", etc.
* Invalid indices: "b1", "c2", or any simple letter+number that isn't a valid fractional index
*
* tldraw uses fractional indexing where indices are strings that can be compared lexicographically
* The format allows inserting new items between any two existing items without renumbering.
* Based on: https://observablehq.com/@dgreensp/implementing-fractional-indexing
*/
function isValidTldrawIndex(index: string): boolean {
if (!index || typeof index !== 'string') return false
if (!index || typeof index !== 'string' || index.length === 0) return false
// Valid tldraw indices start with 'a' and can have various formats:
// "a0", "a1", "a1V", "a1Vz", "Zz", etc.
// The key insight is that indices NOT starting with 'a' (like 'b1', 'c1') are invalid
// unless they're the special "Zz" format used for very high indices
// The first character indicates the integer part length:
// 'a' = 1 digit, 'b' = 2 digits, etc. for positive integers
// 'Z' = 1 digit, 'Y' = 2 digits, etc. for negative integers
// But for normal shapes, 'a' followed by a digit is the most common pattern
// Simple indices like "b1", "c1", "d1" are definitely invalid
if (/^[b-z]\d+$/i.test(index)) {
// Simple patterns that are DEFINITELY invalid for tldraw:
// "b1", "c1", "d1" etc - these are old non-fractional indices (single letter + single digit)
// These were used before tldraw switched to fractional indexing
if (/^[b-z]\d$/i.test(index)) {
return false
}
// An index starting with 'a' followed by digits and optional letters is valid
// e.g., "a0", "a1", "a1V", "a10", "a1Vz"
// Valid tldraw indices should start with lowercase 'a' followed by digits
// and optionally more alphanumeric characters for the fractional/jitter part
// Examples from actual tldraw: "a0", "a1", "a24sT", "a1V4rr"
if (/^a\d/.test(index)) {
return true
}
// Other formats like "Zz" are also valid for high indices
if (/^[A-Z]/.test(index)) {
// Also allow 'Z' prefix for very high indices (though rare)
if (/^Z[a-z]/i.test(index)) {
return true
}
// If none of the above, it's likely invalid
return false
}

View File

@ -145,9 +145,14 @@ export class VideoGenShape extends BaseBoxShapeUtil<IVideoGen> {
}
// Poll for completion
// Video generation can take a long time, especially with GPU cold starts:
// - GPU cold start: 30-120 seconds
// - Model loading: 30-60 seconds
// - Actual generation: 60-180 seconds depending on duration
// Total: up to 6 minutes is reasonable
const statusUrl = `https://api.runpod.ai/v2/${endpointId}/status/${jobData.id}`
let attempts = 0
const maxAttempts = 120 // 4 minutes with 2s intervals (video can take a while)
const maxAttempts = 180 // 6 minutes with 2s intervals
while (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 2000))
@ -203,7 +208,7 @@ export class VideoGenShape extends BaseBoxShapeUtil<IVideoGen> {
}
}
throw new Error('Video generation timed out after 4 minutes')
throw new Error('Video generation timed out after 6 minutes. The GPU may be busy - try again later.')
} catch (error: any) {
const errorMessage = error.message || 'Unknown error during video generation'
console.error('❌ VideoGen: Generation error:', errorMessage)
@ -384,7 +389,10 @@ export class VideoGenShape extends BaseBoxShapeUtil<IVideoGen> {
lineHeight: '1.5'
}}>
<div><strong>Note:</strong> Video generation uses RunPod GPU</div>
<div>Cost: ~$0.50 per video | Processing: 30-90 seconds</div>
<div>Cost: ~$0.50 per video | Processing: 1-5 minutes</div>
<div style={{ marginTop: '4px', opacity: 0.8 }}>
First request may take longer due to GPU cold start
</div>
</div>
</>
)}

View File

@ -1254,10 +1254,21 @@ export class AutomergeDurableObject {
needsUpdate = true
}
// CRITICAL: IndexKey must follow tldraw's fractional indexing format
// Valid format: starts with 'a' followed by digits, optionally followed by uppercase letters
// Examples: "a1", "a2", "a10", "a1V" (fractional between a1 and a2)
// Invalid: "c1", "b1", "z999" (must start with 'a')
if (!record.index || typeof record.index !== 'string' || !/^a\d+[A-Z]*$/.test(record.index)) {
// Valid format: starts with 'a' followed by digits, optionally followed by alphanumeric jitter
// Examples: "a1", "a2", "a10", "a1V", "a24sT", "a1V4rr" (fractional between a1 and a2)
// Invalid: "c1", "b1" (old non-fractional format - single letter + single digit)
// tldraw uses fractional-indexing-jittered library: https://observablehq.com/@dgreensp/implementing-fractional-indexing
const isValidIndex = (idx: any): boolean => {
if (!idx || typeof idx !== 'string' || idx.length === 0) return false
// Old format "b1", "c1" etc are invalid (single letter + single digit)
if (/^[b-z]\d$/i.test(idx)) return false
// Valid: starts with 'a' followed by at least one digit
if (/^a\d/.test(idx)) return true
// Also allow 'Z' prefix for very high indices
if (/^Z[a-z]/i.test(idx)) return true
return false
}
if (!isValidIndex(record.index)) {
console.log(`🔧 Server: Fixing invalid index "${record.index}" to "a1" for shape ${record.id}`)
record.index = 'a1' // Required index property for all shapes - must be valid IndexKey format
needsUpdate = true