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 } sanitized.meta = { ...sanitized.meta }
} }
// CRITICAL: IndexKey must follow tldraw's fractional indexing format // CRITICAL: IndexKey must follow tldraw's fractional indexing format
// Valid format: starts with 'a' followed by digits, optionally followed by uppercase letters // Valid format: starts with 'a' followed by digits, optionally followed by alphanumeric jitter
// Examples: "a1", "a2", "a10", "a1V" (fractional between a1 and a2) // Examples: "a1", "a2", "a10", "a1V", "a24sT", "a1V4rr" (fractional between a1 and a2)
// Invalid: "c1", "b1", "z999" (must start with 'a') // Invalid: "c1", "b1", "z999" (old format - not valid fractional indices)
if (!sanitized.index || typeof sanitized.index !== 'string' || !/^a\d+[A-Z]*$/.test(sanitized.index)) { if (!isValidIndexKey(sanitized.index)) {
sanitized.index = 'a1' 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.parentId) sanitized.parentId = 'page:page'
if (!sanitized.props || typeof sanitized.props !== 'object') sanitized.props = {} 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 * Validate if an index is a valid tldraw fractional index
* Valid indices: "a0", "a1", "a1V", "a2", "Zz", etc. * Valid indices: "a0", "a1", "a1V", "a24sT", "a1V4rr", "Zz", etc.
* Invalid indices: "b1", "c2", or any simple letter+number that isn't "a" followed by proper format * 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 * 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. * 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 { 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: // The first character indicates the integer part length:
// "a0", "a1", "a1V", "a1Vz", "Zz", etc. // 'a' = 1 digit, 'b' = 2 digits, etc. for positive integers
// The key insight is that indices NOT starting with 'a' (like 'b1', 'c1') are invalid // 'Z' = 1 digit, 'Y' = 2 digits, etc. for negative integers
// unless they're the special "Zz" format used for very high indices // But for normal shapes, 'a' followed by a digit is the most common pattern
// Simple indices like "b1", "c1", "d1" are definitely invalid // Simple patterns that are DEFINITELY invalid for tldraw:
if (/^[b-z]\d+$/i.test(index)) { // "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 return false
} }
// An index starting with 'a' followed by digits and optional letters is valid // Valid tldraw indices should start with lowercase 'a' followed by digits
// e.g., "a0", "a1", "a1V", "a10", "a1Vz" // and optionally more alphanumeric characters for the fractional/jitter part
// Examples from actual tldraw: "a0", "a1", "a24sT", "a1V4rr"
if (/^a\d/.test(index)) { if (/^a\d/.test(index)) {
return true return true
} }
// Other formats like "Zz" are also valid for high indices // Also allow 'Z' prefix for very high indices (though rare)
if (/^[A-Z]/.test(index)) { if (/^Z[a-z]/i.test(index)) {
return true return true
} }
// If none of the above, it's likely invalid
return false return false
} }

View File

@ -145,9 +145,14 @@ export class VideoGenShape extends BaseBoxShapeUtil<IVideoGen> {
} }
// Poll for completion // 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}` const statusUrl = `https://api.runpod.ai/v2/${endpointId}/status/${jobData.id}`
let attempts = 0 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) { while (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 2000)) 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) { } catch (error: any) {
const errorMessage = error.message || 'Unknown error during video generation' const errorMessage = error.message || 'Unknown error during video generation'
console.error('❌ VideoGen: Generation error:', errorMessage) console.error('❌ VideoGen: Generation error:', errorMessage)
@ -384,7 +389,10 @@ export class VideoGenShape extends BaseBoxShapeUtil<IVideoGen> {
lineHeight: '1.5' lineHeight: '1.5'
}}> }}>
<div><strong>Note:</strong> Video generation uses RunPod GPU</div> <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> </div>
</> </>
)} )}

View File

@ -1254,10 +1254,21 @@ export class AutomergeDurableObject {
needsUpdate = true needsUpdate = true
} }
// CRITICAL: IndexKey must follow tldraw's fractional indexing format // CRITICAL: IndexKey must follow tldraw's fractional indexing format
// Valid format: starts with 'a' followed by digits, optionally followed by uppercase letters // Valid format: starts with 'a' followed by digits, optionally followed by alphanumeric jitter
// Examples: "a1", "a2", "a10", "a1V" (fractional between a1 and a2) // Examples: "a1", "a2", "a10", "a1V", "a24sT", "a1V4rr" (fractional between a1 and a2)
// Invalid: "c1", "b1", "z999" (must start with 'a') // Invalid: "c1", "b1" (old non-fractional format - single letter + single digit)
if (!record.index || typeof record.index !== 'string' || !/^a\d+[A-Z]*$/.test(record.index)) { // 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}`) 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 record.index = 'a1' // Required index property for all shapes - must be valid IndexKey format
needsUpdate = true needsUpdate = true