Implement job metadata extraction and task management features. Add validation for frame range limits, enhance job and task data structures, and introduce new API endpoints for metadata and task retrieval. Update client-side components to handle metadata extraction and display task statuses. Improve error handling in API responses.

This commit is contained in:
2025-11-22 06:37:32 -06:00
parent 27a09aedd6
commit c9ade39ad9
10 changed files with 1078 additions and 88 deletions

View File

@@ -5,6 +5,7 @@ import VideoPlayer from './VideoPlayer';
export default function JobDetails({ job, onClose, onUpdate }) {
const [jobDetails, setJobDetails] = useState(job);
const [files, setFiles] = useState([]);
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
const [videoUrl, setVideoUrl] = useState(null);
const [selectedTaskId, setSelectedTaskId] = useState(null);
@@ -42,12 +43,14 @@ export default function JobDetails({ job, onClose, onUpdate }) {
const loadDetails = async () => {
try {
const [details, fileList] = await Promise.all([
const [details, fileList, taskList] = await Promise.all([
jobs.get(job.id),
jobs.getFiles(job.id),
jobs.getTasks(job.id),
]);
setJobDetails(details);
setFiles(fileList);
setTasks(taskList);
// Check if there's an MP4 output file
const mp4File = fileList.find(
@@ -151,6 +154,16 @@ export default function JobDetails({ job, onClose, onUpdate }) {
}
};
const getTaskStatusColor = (status) => {
const colors = {
pending: 'bg-yellow-100 text-yellow-800',
running: 'bg-blue-100 text-blue-800',
completed: 'bg-green-100 text-green-800',
failed: 'bg-red-100 text-red-800',
};
return colors[status] || 'bg-gray-100 text-gray-800';
};
const outputFiles = files.filter((f) => f.file_type === 'output');
const inputFiles = files.filter((f) => f.file_type === 'input');
@@ -269,9 +282,42 @@ export default function JobDetails({ job, onClose, onUpdate }) {
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">
Task Execution
Tasks
</h3>
<div className="space-y-4">
{tasks.length > 0 && (
<div className="bg-gray-50 rounded-lg p-4 mb-4">
<h4 className="font-medium text-gray-900 mb-2">Task List</h4>
<div className="space-y-2 max-h-64 overflow-y-auto">
{tasks.map((task) => (
<div
key={task.id}
onClick={() => handleTaskClick(task.id)}
className={`flex items-center justify-between p-3 bg-white rounded cursor-pointer hover:bg-gray-100 transition-colors ${
selectedTaskId === task.id ? 'ring-2 ring-purple-600' : ''
}`}
>
<div className="flex items-center gap-3">
<span className={`px-2 py-1 rounded text-xs font-medium ${getTaskStatusColor(task.status)}`}>
{task.status}
</span>
<span className="font-medium text-gray-900">
Frame {task.frame_start}
{task.frame_end !== task.frame_start ? `-${task.frame_end}` : ''}
</span>
{task.task_type && task.task_type !== 'render' && (
<span className="text-xs text-gray-500">({task.task_type})</span>
)}
</div>
<div className="text-sm text-gray-600">
{task.runner_id && `Runner ${task.runner_id}`}
</div>
</div>
))}
</div>
</div>
)}
{taskSteps.length > 0 && (
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="font-medium text-gray-900 mb-2">Steps</h4>

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { jobs } from '../utils/api';
export default function JobSubmission({ onSuccess }) {
@@ -12,6 +12,113 @@ export default function JobSubmission({ onSuccess }) {
const [file, setFile] = useState(null);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
const [metadataStatus, setMetadataStatus] = useState(null); // 'extracting', 'completed', 'error'
const [metadata, setMetadata] = useState(null);
const [currentJobId, setCurrentJobId] = useState(null);
// Poll for metadata after file upload
useEffect(() => {
if (!currentJobId || metadataStatus !== 'extracting') return;
let pollCount = 0;
const maxPolls = 30; // 60 seconds max (30 * 2 seconds)
let timeoutId = null;
const pollMetadata = async () => {
pollCount++;
// Stop polling after timeout
if (pollCount > maxPolls) {
setMetadataStatus('error');
// Cancel temp job on timeout
try {
await jobs.cancel(currentJobId);
} catch (err) {
// Ignore errors when canceling
}
return;
}
try {
const metadata = await jobs.getMetadata(currentJobId);
if (metadata) {
setMetadata(metadata);
setMetadataStatus('completed');
// Auto-populate form fields
setFormData(prev => ({
...prev,
frame_start: metadata.frame_start || prev.frame_start,
frame_end: metadata.frame_end || prev.frame_end,
output_format: metadata.render_settings?.output_format || prev.output_format,
}));
}
} catch (err) {
// Metadata not ready yet, continue polling (only if 404/not found)
if (err.message.includes('404') || err.message.includes('not found')) {
// Continue polling via interval
} else {
setMetadataStatus('error');
}
}
};
const interval = setInterval(pollMetadata, 2000);
// Set timeout to stop polling after 60 seconds
timeoutId = setTimeout(() => {
clearInterval(interval);
if (metadataStatus === 'extracting') {
setMetadataStatus('error');
// Cancel temp job on timeout
jobs.cancel(currentJobId).catch(() => {});
}
}, 60000);
return () => {
clearInterval(interval);
if (timeoutId) clearTimeout(timeoutId);
// Cleanup: cancel temp job if component unmounts during extraction
if (currentJobId && metadataStatus === 'extracting') {
jobs.cancel(currentJobId).catch(() => {});
}
};
}, [currentJobId, metadataStatus]);
const handleFileChange = async (e) => {
const selectedFile = e.target.files[0];
if (!selectedFile) {
setFile(null);
return;
}
setFile(selectedFile);
setMetadataStatus(null);
setMetadata(null);
setCurrentJobId(null);
// If it's a blend file, create a temporary job to extract metadata
if (selectedFile.name.toLowerCase().endsWith('.blend')) {
try {
// Create a temporary job for metadata extraction
const tempJob = await jobs.create({
name: 'Metadata Extraction',
frame_start: 1,
frame_end: 10,
output_format: 'PNG',
allow_parallel_runners: true,
});
setCurrentJobId(tempJob.id);
setMetadataStatus('extracting');
// Upload file to trigger metadata extraction
await jobs.uploadFile(tempJob.id, selectedFile);
} catch (err) {
console.error('Failed to start metadata extraction:', err);
setMetadataStatus('error');
}
}
};
const handleSubmit = async (e) => {
e.preventDefault();
@@ -27,7 +134,16 @@ export default function JobSubmission({ onSuccess }) {
throw new Error('Invalid frame range');
}
// Create job
// If we have a temporary job for metadata extraction, cancel it
if (currentJobId) {
try {
await jobs.cancel(currentJobId);
} catch (err) {
// Ignore errors when canceling temp job
}
}
// Create actual job
const job = await jobs.create({
name: formData.name,
frame_start: parseInt(formData.frame_start),
@@ -48,6 +164,9 @@ export default function JobSubmission({ onSuccess }) {
allow_parallel_runners: true,
});
setFile(null);
setMetadata(null);
setMetadataStatus(null);
setCurrentJobId(null);
e.target.reset();
if (onSuccess) {
@@ -150,10 +269,37 @@ export default function JobSubmission({ onSuccess }) {
<input
type="file"
accept=".blend"
onChange={(e) => setFile(e.target.files[0])}
onChange={handleFileChange}
required
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-600 focus:border-transparent file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-purple-50 file:text-purple-700 hover:file:bg-purple-100"
/>
{metadataStatus === 'extracting' && (
<div className="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg text-blue-700 text-sm">
Extracting metadata from blend file...
</div>
)}
{metadataStatus === 'completed' && metadata && (
<div className="mt-2 p-3 bg-green-50 border border-green-200 rounded-lg text-sm">
<div className="text-green-700 font-semibold mb-1">Metadata extracted successfully!</div>
<div className="text-green-600 text-xs space-y-1">
<div>Frames: {metadata.frame_start} - {metadata.frame_end}</div>
<div>Resolution: {metadata.render_settings?.resolution_x} x {metadata.render_settings?.resolution_y}</div>
<div>Engine: {metadata.render_settings?.engine}</div>
<div>Samples: {metadata.render_settings?.samples}</div>
<div className="text-gray-600 mt-2">Form fields have been auto-populated. You can adjust them if needed.</div>
{(formData.frame_start < metadata.frame_start || formData.frame_end > metadata.frame_end) && (
<div className="mt-2 p-2 bg-yellow-50 border border-yellow-200 rounded text-yellow-800 text-xs">
<strong>Warning:</strong> Your frame range ({formData.frame_start}-{formData.frame_end}) exceeds the blend file's range ({metadata.frame_start}-{metadata.frame_end}). This may cause errors.
</div>
)}
</div>
</div>
)}
{metadataStatus === 'error' && (
<div className="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-yellow-700 text-sm">
Could not extract metadata. Please fill in the form manually.
</div>
)}
</div>
<button