Spaces:
Running
Running
Commit
·
66e744c
1
Parent(s):
8e0b1d0
Add video cancellation feature and update API integration
Browse files- Implemented a new endpoint to cancel ongoing video generation tasks in `video_generation.py`.
- Enhanced the frontend to manage task IDs and cancellation state in `GenerationContext.tsx` and `GenerationForm.tsx`.
- Updated the UI to allow users to cancel video generation with a button in `GenerationProgress.tsx`.
- Added support for handling cancellation in the video generation process.
- Included new dependencies in `requirements.txt` for OpenAI Whisper.
- api/image_service.py +1 -1
- api/video_generation.py +39 -0
- frontend/src/components/GenerationForm.tsx +28 -8
- frontend/src/components/GenerationProgress.tsx +35 -10
- frontend/src/context/GenerationContext.tsx +49 -0
- frontend/src/types/index.ts +2 -0
- frontend/src/utils/api.ts +11 -0
- requirements.txt +1 -0
api/image_service.py
CHANGED
|
@@ -26,7 +26,7 @@ async def upload_image(file: UploadFile = File(...)):
|
|
| 26 |
data_url = f"data:{file.content_type};base64,{encoded}"
|
| 27 |
|
| 28 |
# Get public URL from env or use default
|
| 29 |
-
public_url = os.getenv('
|
| 30 |
|
| 31 |
# Compress and store, get hosted URL
|
| 32 |
hosted_url = await compress_and_store_image(data_url, public_url)
|
|
|
|
| 26 |
data_url = f"data:{file.content_type};base64,{encoded}"
|
| 27 |
|
| 28 |
# Get public URL from env or use default
|
| 29 |
+
public_url = os.getenv('VITE_API_BASE_URL', 'http://localhost:4000')
|
| 30 |
|
| 31 |
# Compress and store, get hosted URL
|
| 32 |
hosted_url = await compress_and_store_image(data_url, public_url)
|
api/video_generation.py
CHANGED
|
@@ -15,6 +15,7 @@ from datetime import datetime
|
|
| 15 |
|
| 16 |
from utils.image_processor import compress_and_store_image
|
| 17 |
from utils.storage import video_results, sse_clients, cleanup_old_results
|
|
|
|
| 18 |
|
| 19 |
router = APIRouter()
|
| 20 |
|
|
@@ -575,6 +576,44 @@ async def get_video_status(task_id: str):
|
|
| 575 |
detail=f"Failed to check video status: {error_msg}"
|
| 576 |
)
|
| 577 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
@router.get("/veo/download")
|
| 579 |
async def download_video(url: str):
|
| 580 |
"""
|
|
|
|
| 15 |
|
| 16 |
from utils.image_processor import compress_and_store_image
|
| 17 |
from utils.storage import video_results, sse_clients, cleanup_old_results
|
| 18 |
+
import asyncio
|
| 19 |
|
| 20 |
router = APIRouter()
|
| 21 |
|
|
|
|
| 576 |
detail=f"Failed to check video status: {error_msg}"
|
| 577 |
)
|
| 578 |
|
| 579 |
+
@router.post("/veo/cancel/{task_id}")
|
| 580 |
+
async def cancel_video_generation(task_id: str):
|
| 581 |
+
"""
|
| 582 |
+
Cancel an ongoing video generation task
|
| 583 |
+
"""
|
| 584 |
+
try:
|
| 585 |
+
# Send cancellation event to SSE clients
|
| 586 |
+
if task_id in sse_clients:
|
| 587 |
+
queue = sse_clients[task_id]
|
| 588 |
+
await send_sse_event(task_id, {
|
| 589 |
+
'status': 'cancelled',
|
| 590 |
+
'message': 'Video generation cancelled by user'
|
| 591 |
+
})
|
| 592 |
+
# Close the SSE connection
|
| 593 |
+
del sse_clients[task_id]
|
| 594 |
+
print(f"✅ Cancelled video generation: {task_id}")
|
| 595 |
+
|
| 596 |
+
# Mark result as cancelled
|
| 597 |
+
if task_id in video_results:
|
| 598 |
+
video_results[task_id] = {
|
| 599 |
+
'code': 499, # Client Closed Request
|
| 600 |
+
'msg': 'Video generation cancelled by user',
|
| 601 |
+
'taskId': task_id,
|
| 602 |
+
'timestamp': datetime.now().timestamp()
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
return JSONResponse(
|
| 606 |
+
status_code=200,
|
| 607 |
+
content={'code': 200, 'msg': 'Video generation cancelled', 'taskId': task_id}
|
| 608 |
+
)
|
| 609 |
+
|
| 610 |
+
except Exception as e:
|
| 611 |
+
print(f"❌ Error cancelling video generation: {str(e)}")
|
| 612 |
+
raise HTTPException(
|
| 613 |
+
status_code=500,
|
| 614 |
+
detail=f"Failed to cancel video generation: {str(e)}"
|
| 615 |
+
)
|
| 616 |
+
|
| 617 |
@router.get("/veo/download")
|
| 618 |
async def download_video(url: str):
|
| 619 |
"""
|
frontend/src/components/GenerationForm.tsx
CHANGED
|
@@ -37,8 +37,8 @@ const aspectRatios = ['9:16', '16:9', '1:1'];
|
|
| 37 |
type GenerationMode = 'extend' | 'frame-continuity';
|
| 38 |
|
| 39 |
export const GenerationForm: React.FC<GenerationFormProps> = ({ provider, onBack }) => {
|
| 40 |
-
const { startGeneration, updateProgress, addVideo, setStep, setError, setRetryState, updateSegments, state } = useGeneration();
|
| 41 |
-
const { retryState, generatedVideos, segments } = state;
|
| 42 |
|
| 43 |
// Draft storage key
|
| 44 |
const draftKey = `video-gen-draft-${provider}`;
|
|
@@ -554,6 +554,11 @@ export const GenerationForm: React.FC<GenerationFormProps> = ({ provider, onBack
|
|
| 554 |
|
| 555 |
// Generate video with automatic retry (retries once on failure)
|
| 556 |
updateProgress(`Processing video ${i + 1}... (this may take 1-2 minutes)`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
const videoUrl = await generateVideoWithRetry(async () => {
|
| 558 |
if (i === 0 || (i === startIndex && startIndex > 0)) {
|
| 559 |
// First segment OR resuming after failure: use generate API with current image
|
|
@@ -567,6 +572,7 @@ export const GenerationForm: React.FC<GenerationFormProps> = ({ provider, onBack
|
|
| 567 |
voiceType: formState.voiceType,
|
| 568 |
});
|
| 569 |
currentTaskId = generateResult.taskId;
|
|
|
|
| 570 |
return generateResult;
|
| 571 |
} else {
|
| 572 |
// Subsequent segments: use extend API
|
|
@@ -577,6 +583,7 @@ export const GenerationForm: React.FC<GenerationFormProps> = ({ provider, onBack
|
|
| 577 |
formState.voiceType
|
| 578 |
);
|
| 579 |
currentTaskId = extendResult.taskId;
|
|
|
|
| 580 |
return extendResult;
|
| 581 |
}
|
| 582 |
}, 300000, (attempt) => {
|
|
@@ -602,6 +609,11 @@ export const GenerationForm: React.FC<GenerationFormProps> = ({ provider, onBack
|
|
| 602 |
});
|
| 603 |
|
| 604 |
updateProgress(`Completed video ${i + 1} of ${payload.segments.length}`, i + 1, payload.segments.length);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
}
|
| 606 |
|
| 607 |
clearDraft(); // Clear draft on successful generation
|
|
@@ -612,15 +624,23 @@ export const GenerationForm: React.FC<GenerationFormProps> = ({ provider, onBack
|
|
| 612 |
console.error('Generation error:', err);
|
| 613 |
const errorMessage = err instanceof Error ? err.message : 'Generation failed';
|
| 614 |
|
| 615 |
-
//
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
error
|
| 619 |
-
}
|
| 620 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
|
| 622 |
} finally {
|
| 623 |
setIsGenerating(false);
|
|
|
|
|
|
|
| 624 |
}
|
| 625 |
};
|
| 626 |
|
|
|
|
| 37 |
type GenerationMode = 'extend' | 'frame-continuity';
|
| 38 |
|
| 39 |
export const GenerationForm: React.FC<GenerationFormProps> = ({ provider, onBack }) => {
|
| 40 |
+
const { startGeneration, updateProgress, addVideo, setStep, setError, setRetryState, updateSegments, addTaskId, removeTaskId, state } = useGeneration();
|
| 41 |
+
const { retryState, generatedVideos, segments, isCancelling } = state;
|
| 42 |
|
| 43 |
// Draft storage key
|
| 44 |
const draftKey = `video-gen-draft-${provider}`;
|
|
|
|
| 554 |
|
| 555 |
// Generate video with automatic retry (retries once on failure)
|
| 556 |
updateProgress(`Processing video ${i + 1}... (this may take 1-2 minutes)`);
|
| 557 |
+
// Check if cancelled
|
| 558 |
+
if (isCancelling) {
|
| 559 |
+
throw new Error('Generation cancelled by user');
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
const videoUrl = await generateVideoWithRetry(async () => {
|
| 563 |
if (i === 0 || (i === startIndex && startIndex > 0)) {
|
| 564 |
// First segment OR resuming after failure: use generate API with current image
|
|
|
|
| 572 |
voiceType: formState.voiceType,
|
| 573 |
});
|
| 574 |
currentTaskId = generateResult.taskId;
|
| 575 |
+
addTaskId(currentTaskId);
|
| 576 |
return generateResult;
|
| 577 |
} else {
|
| 578 |
// Subsequent segments: use extend API
|
|
|
|
| 583 |
formState.voiceType
|
| 584 |
);
|
| 585 |
currentTaskId = extendResult.taskId;
|
| 586 |
+
addTaskId(currentTaskId);
|
| 587 |
return extendResult;
|
| 588 |
}
|
| 589 |
}, 300000, (attempt) => {
|
|
|
|
| 609 |
});
|
| 610 |
|
| 611 |
updateProgress(`Completed video ${i + 1} of ${payload.segments.length}`, i + 1, payload.segments.length);
|
| 612 |
+
|
| 613 |
+
// Remove task ID after completion
|
| 614 |
+
if (currentTaskId) {
|
| 615 |
+
removeTaskId(currentTaskId);
|
| 616 |
+
}
|
| 617 |
}
|
| 618 |
|
| 619 |
clearDraft(); // Clear draft on successful generation
|
|
|
|
| 624 |
console.error('Generation error:', err);
|
| 625 |
const errorMessage = err instanceof Error ? err.message : 'Generation failed';
|
| 626 |
|
| 627 |
+
// If cancelled, don't show retry option
|
| 628 |
+
if (errorMessage.includes('cancelled') || isCancelling) {
|
| 629 |
+
setError('Generation cancelled by user');
|
| 630 |
+
setStep('error');
|
| 631 |
+
} else {
|
| 632 |
+
// Enable retry mode
|
| 633 |
+
setRetryState({
|
| 634 |
+
failedSegmentIndex: generatedVideos.length, // Current segment that failed
|
| 635 |
+
error: errorMessage
|
| 636 |
+
});
|
| 637 |
+
setStep('configuring'); // Go back to form, but with retry overlay
|
| 638 |
+
}
|
| 639 |
|
| 640 |
} finally {
|
| 641 |
setIsGenerating(false);
|
| 642 |
+
// Clean up any remaining task IDs
|
| 643 |
+
state.activeTaskIds.forEach(taskId => removeTaskId(taskId));
|
| 644 |
}
|
| 645 |
};
|
| 646 |
|
frontend/src/components/GenerationProgress.tsx
CHANGED
|
@@ -61,9 +61,15 @@ interface ActivityLog {
|
|
| 61 |
icon?: 'video' | 'audio' | 'brain' | 'download' | 'image';
|
| 62 |
}
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
export const GenerationProgress: React.FC = () => {
|
| 65 |
-
const { state } = useGeneration();
|
| 66 |
-
const { progress, provider, generatedVideos, segments } = state;
|
| 67 |
|
| 68 |
const [elapsedTime, setElapsedTime] = useState(0);
|
| 69 |
const [activityLog, setActivityLog] = useState<ActivityLog[]>([]);
|
|
@@ -176,18 +182,37 @@ export const GenerationProgress: React.FC = () => {
|
|
| 176 |
<div>
|
| 177 |
<h2 className="text-2xl font-bold text-void-100">Generating Videos</h2>
|
| 178 |
<p className="text-void-400 text-sm mt-1">
|
| 179 |
-
{provider === 'kling' ? '
|
| 180 |
</p>
|
| 181 |
</div>
|
| 182 |
-
<div className="flex items-center gap-
|
| 183 |
-
<div className="flex items-center gap-
|
| 184 |
-
<ClockIcon />
|
| 185 |
-
<span>Elapsed: <span className="text-void-200 font-mono">{formatTime(elapsedTime)}</span></span>
|
| 186 |
-
</div>
|
| 187 |
-
{progress.current > 0 && remainingTime > 0 && (
|
| 188 |
<div className="flex items-center gap-2 text-void-400">
|
| 189 |
-
<
|
|
|
|
| 190 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
)}
|
| 192 |
</div>
|
| 193 |
</div>
|
|
|
|
| 61 |
icon?: 'video' | 'audio' | 'brain' | 'download' | 'image';
|
| 62 |
}
|
| 63 |
|
| 64 |
+
const XIcon = () => (
|
| 65 |
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 66 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
| 67 |
+
</svg>
|
| 68 |
+
);
|
| 69 |
+
|
| 70 |
export const GenerationProgress: React.FC = () => {
|
| 71 |
+
const { state, cancelGeneration } = useGeneration();
|
| 72 |
+
const { progress, provider, generatedVideos, segments, isCancelling, activeTaskIds } = state;
|
| 73 |
|
| 74 |
const [elapsedTime, setElapsedTime] = useState(0);
|
| 75 |
const [activityLog, setActivityLog] = useState<ActivityLog[]>([]);
|
|
|
|
| 182 |
<div>
|
| 183 |
<h2 className="text-2xl font-bold text-void-100">Generating Videos</h2>
|
| 184 |
<p className="text-void-400 text-sm mt-1">
|
| 185 |
+
{provider === 'kling' ? 'KIE API' : 'Replicate'} • {segments.length} segments
|
| 186 |
</p>
|
| 187 |
</div>
|
| 188 |
+
<div className="flex items-center gap-4">
|
| 189 |
+
<div className="flex items-center gap-6 text-sm">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
<div className="flex items-center gap-2 text-void-400">
|
| 191 |
+
<ClockIcon />
|
| 192 |
+
<span>Elapsed: <span className="text-void-200 font-mono">{formatTime(elapsedTime)}</span></span>
|
| 193 |
</div>
|
| 194 |
+
{progress.current > 0 && remainingTime > 0 && (
|
| 195 |
+
<div className="flex items-center gap-2 text-void-400">
|
| 196 |
+
<span>Est. remaining: <span className={`font-mono ${accentClass}`}>{formatTime(remainingTime)}</span></span>
|
| 197 |
+
</div>
|
| 198 |
+
)}
|
| 199 |
+
</div>
|
| 200 |
+
{activeTaskIds.length > 0 && (
|
| 201 |
+
<button
|
| 202 |
+
onClick={cancelGeneration}
|
| 203 |
+
disabled={isCancelling}
|
| 204 |
+
className={`
|
| 205 |
+
px-4 py-2 rounded-lg font-medium text-sm transition-all
|
| 206 |
+
flex items-center gap-2
|
| 207 |
+
${isCancelling
|
| 208 |
+
? 'bg-void-700 text-void-400 cursor-not-allowed'
|
| 209 |
+
: 'bg-red-500/20 hover:bg-red-500/30 text-red-400 hover:text-red-300 border border-red-500/30'
|
| 210 |
+
}
|
| 211 |
+
`}
|
| 212 |
+
>
|
| 213 |
+
<XIcon />
|
| 214 |
+
{isCancelling ? 'Cancelling...' : 'Cancel Generation'}
|
| 215 |
+
</button>
|
| 216 |
)}
|
| 217 |
</div>
|
| 218 |
</div>
|
frontend/src/context/GenerationContext.tsx
CHANGED
|
@@ -22,6 +22,8 @@ const initialState: GenerationState = {
|
|
| 22 |
error: null,
|
| 23 |
taskId: null,
|
| 24 |
retryState: null,
|
|
|
|
|
|
|
| 25 |
};
|
| 26 |
|
| 27 |
// Action types
|
|
@@ -35,6 +37,9 @@ type GenerationAction =
|
|
| 35 |
| { type: 'SET_ERROR'; payload: string | null }
|
| 36 |
| { type: 'SET_TASK_ID'; payload: string | null }
|
| 37 |
| { type: 'SET_RETRY_STATE'; payload: { failedSegmentIndex: number; error: string } | null }
|
|
|
|
|
|
|
|
|
|
| 38 |
| { type: 'RESET' };
|
| 39 |
|
| 40 |
// Reducer
|
|
@@ -64,6 +69,12 @@ function generationReducer(state: GenerationState, action: GenerationAction): Ge
|
|
| 64 |
return { ...state, taskId: action.payload };
|
| 65 |
case 'SET_RETRY_STATE':
|
| 66 |
return { ...state, retryState: action.payload };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
case 'RESET':
|
| 68 |
return { ...initialState, provider: state.provider };
|
| 69 |
default:
|
|
@@ -86,6 +97,9 @@ interface GenerationContextValue {
|
|
| 86 |
setError: (error: string | null) => void;
|
| 87 |
setRetryState: (state: { failedSegmentIndex: number; error: string } | null) => void;
|
| 88 |
updateSegments: (segments: VeoSegment[]) => void;
|
|
|
|
|
|
|
|
|
|
| 89 |
reset: () => void;
|
| 90 |
}
|
| 91 |
|
|
@@ -148,6 +162,41 @@ export function GenerationProvider({ children }: { children: ReactNode }) {
|
|
| 148 |
dispatch({ type: 'SET_SEGMENTS', payload: segments });
|
| 149 |
},
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
reset: () => {
|
| 152 |
dispatch({ type: 'RESET' });
|
| 153 |
},
|
|
|
|
| 22 |
error: null,
|
| 23 |
taskId: null,
|
| 24 |
retryState: null,
|
| 25 |
+
activeTaskIds: [] as string[],
|
| 26 |
+
isCancelling: false,
|
| 27 |
};
|
| 28 |
|
| 29 |
// Action types
|
|
|
|
| 37 |
| { type: 'SET_ERROR'; payload: string | null }
|
| 38 |
| { type: 'SET_TASK_ID'; payload: string | null }
|
| 39 |
| { type: 'SET_RETRY_STATE'; payload: { failedSegmentIndex: number; error: string } | null }
|
| 40 |
+
| { type: 'ADD_TASK_ID'; payload: string }
|
| 41 |
+
| { type: 'REMOVE_TASK_ID'; payload: string }
|
| 42 |
+
| { type: 'SET_CANCELLING'; payload: boolean }
|
| 43 |
| { type: 'RESET' };
|
| 44 |
|
| 45 |
// Reducer
|
|
|
|
| 69 |
return { ...state, taskId: action.payload };
|
| 70 |
case 'SET_RETRY_STATE':
|
| 71 |
return { ...state, retryState: action.payload };
|
| 72 |
+
case 'ADD_TASK_ID':
|
| 73 |
+
return { ...state, activeTaskIds: [...state.activeTaskIds, action.payload] };
|
| 74 |
+
case 'REMOVE_TASK_ID':
|
| 75 |
+
return { ...state, activeTaskIds: state.activeTaskIds.filter(id => id !== action.payload) };
|
| 76 |
+
case 'SET_CANCELLING':
|
| 77 |
+
return { ...state, isCancelling: action.payload };
|
| 78 |
case 'RESET':
|
| 79 |
return { ...initialState, provider: state.provider };
|
| 80 |
default:
|
|
|
|
| 97 |
setError: (error: string | null) => void;
|
| 98 |
setRetryState: (state: { failedSegmentIndex: number; error: string } | null) => void;
|
| 99 |
updateSegments: (segments: VeoSegment[]) => void;
|
| 100 |
+
addTaskId: (taskId: string) => void;
|
| 101 |
+
removeTaskId: (taskId: string) => void;
|
| 102 |
+
cancelGeneration: () => Promise<void>;
|
| 103 |
reset: () => void;
|
| 104 |
}
|
| 105 |
|
|
|
|
| 162 |
dispatch({ type: 'SET_SEGMENTS', payload: segments });
|
| 163 |
},
|
| 164 |
|
| 165 |
+
addTaskId: (taskId) => {
|
| 166 |
+
dispatch({ type: 'ADD_TASK_ID', payload: taskId });
|
| 167 |
+
},
|
| 168 |
+
|
| 169 |
+
removeTaskId: (taskId) => {
|
| 170 |
+
dispatch({ type: 'REMOVE_TASK_ID', payload: taskId });
|
| 171 |
+
},
|
| 172 |
+
|
| 173 |
+
cancelGeneration: async () => {
|
| 174 |
+
dispatch({ type: 'SET_CANCELLING', payload: true });
|
| 175 |
+
try {
|
| 176 |
+
const { klingCancel } = await import('@/utils/api');
|
| 177 |
+
// Cancel all active tasks
|
| 178 |
+
const currentTaskIds = [...state.activeTaskIds];
|
| 179 |
+
const cancelPromises = currentTaskIds.map(taskId =>
|
| 180 |
+
klingCancel(taskId).catch(err => {
|
| 181 |
+
console.warn(`Failed to cancel task ${taskId}:`, err);
|
| 182 |
+
})
|
| 183 |
+
);
|
| 184 |
+
await Promise.all(cancelPromises);
|
| 185 |
+
// Clear all task IDs
|
| 186 |
+
currentTaskIds.forEach(id => {
|
| 187 |
+
dispatch({ type: 'REMOVE_TASK_ID', payload: id });
|
| 188 |
+
});
|
| 189 |
+
dispatch({ type: 'SET_TASK_ID', payload: null });
|
| 190 |
+
dispatch({ type: 'SET_ERROR', payload: 'Generation cancelled by user' });
|
| 191 |
+
dispatch({ type: 'SET_STEP', payload: 'error' });
|
| 192 |
+
} catch (error) {
|
| 193 |
+
console.error('Error cancelling generation:', error);
|
| 194 |
+
dispatch({ type: 'SET_ERROR', payload: 'Failed to cancel generation' });
|
| 195 |
+
} finally {
|
| 196 |
+
dispatch({ type: 'SET_CANCELLING', payload: false });
|
| 197 |
+
}
|
| 198 |
+
},
|
| 199 |
+
|
| 200 |
reset: () => {
|
| 201 |
dispatch({ type: 'RESET' });
|
| 202 |
},
|
frontend/src/types/index.ts
CHANGED
|
@@ -183,6 +183,8 @@ export interface GenerationState {
|
|
| 183 |
failedSegmentIndex: number;
|
| 184 |
error: string;
|
| 185 |
} | null;
|
|
|
|
|
|
|
| 186 |
}
|
| 187 |
|
| 188 |
export interface GeneratedVideo {
|
|
|
|
| 183 |
failedSegmentIndex: number;
|
| 184 |
error: string;
|
| 185 |
} | null;
|
| 186 |
+
activeTaskIds: string[];
|
| 187 |
+
isCancelling: boolean;
|
| 188 |
}
|
| 189 |
|
| 190 |
export interface GeneratedVideo {
|
frontend/src/utils/api.ts
CHANGED
|
@@ -210,6 +210,13 @@ export async function klingGetStatus(taskId: string): Promise<VideoStatusRespons
|
|
| 210 |
return apiRequest<VideoStatusResponse>(`/api/veo/status/${taskId}`);
|
| 211 |
}
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
export function createKlingEventSource(taskId: string): EventSource {
|
| 214 |
return new EventSource(`${API_BASE}/api/veo/events/${taskId}`);
|
| 215 |
}
|
|
@@ -501,6 +508,10 @@ export function waitForKlingVideo(taskId: string, timeoutMs: number = 300000): P
|
|
| 501 |
clearTimeout(timeout);
|
| 502 |
eventSource.close();
|
| 503 |
resolve(data.url);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
} else if (data.status === 'failed' || data.code !== undefined && data.code !== 200) {
|
| 505 |
clearTimeout(timeout);
|
| 506 |
eventSource.close();
|
|
|
|
| 210 |
return apiRequest<VideoStatusResponse>(`/api/veo/status/${taskId}`);
|
| 211 |
}
|
| 212 |
|
| 213 |
+
export async function klingCancel(taskId: string): Promise<{ code: number; msg: string; taskId: string }> {
|
| 214 |
+
return apiRequest<{ code: number; msg: string; taskId: string }>(`/api/veo/cancel/${taskId}`, {
|
| 215 |
+
method: 'POST',
|
| 216 |
+
headers: { 'Content-Type': 'application/json' },
|
| 217 |
+
});
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
export function createKlingEventSource(taskId: string): EventSource {
|
| 221 |
return new EventSource(`${API_BASE}/api/veo/events/${taskId}`);
|
| 222 |
}
|
|
|
|
| 508 |
clearTimeout(timeout);
|
| 509 |
eventSource.close();
|
| 510 |
resolve(data.url);
|
| 511 |
+
} else if (data.status === 'cancelled') {
|
| 512 |
+
clearTimeout(timeout);
|
| 513 |
+
eventSource.close();
|
| 514 |
+
reject(new Error('Video generation cancelled by user'));
|
| 515 |
} else if (data.status === 'failed' || data.code !== undefined && data.code !== 200) {
|
| 516 |
clearTimeout(timeout);
|
| 517 |
eventSource.close();
|
requirements.txt
CHANGED
|
@@ -36,3 +36,4 @@ python-multipart==0.0.17
|
|
| 36 |
# pytest==8.3.0
|
| 37 |
# pytest-asyncio==0.24.0
|
| 38 |
|
|
|
|
|
|
| 36 |
# pytest==8.3.0
|
| 37 |
# pytest-asyncio==0.24.0
|
| 38 |
|
| 39 |
+
openai-whisper
|