feat(pr4+pr5): hotkeys, unified status labels, upload size constant
PR-4 hotkeys (L-9): - QCDetail: Cmd/Ctrl+S saves current VTT (handleSaveFullVtt) - QCDetail: Escape closes both reject forms (final review + language reject modal) PR-5 T-1 (unified status labels): - Add JOB_STATUS_LABELS and getJobStatusLabel to utils/jobStatusMessages.ts - JobsList.tsx: remove local STATUS_LABELS duplicate, import from shared util - StatusBadge.tsx: remove 30-line switch duplicate, use getJobStatusLabel PR-5 T-14 (unified upload size constant): - config.py: upload_max_video_bytes = 2GB, upload_signed_url_ttl_hours = 24 - validation.py: use settings.upload_max_video_bytes instead of magic number - notify.py: use settings.upload_signed_url_ttl_hours for signed URL TTL Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
13db347d65
commit
1bf0fb9eed
7 changed files with 73 additions and 73 deletions
|
|
@ -288,6 +288,10 @@ class Settings(BaseSettings):
|
|||
cost_tracker_source_app: str = "video-accessibility"
|
||||
cost_tracker_enabled: bool = True
|
||||
|
||||
# Upload limits (T-14 — single source of truth)
|
||||
upload_max_video_bytes: int = 2 * 1024 * 1024 * 1024 # 2GB
|
||||
upload_signed_url_ttl_hours: int = 24 # signed URL lifetime
|
||||
|
||||
# CORS - comma-separated list of allowed origins
|
||||
cors_origins: str = "http://localhost:5173,http://localhost:5174,http://localhost:3000,http://localhost:6001"
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import json
|
||||
import re
|
||||
import time
|
||||
from ..core.config import settings
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
from fastapi import HTTPException, Request, status
|
||||
from fastapi.responses import JSONResponse
|
||||
|
|
@ -67,8 +68,8 @@ class RequestValidator:
|
|||
|
||||
self.compiled_patterns = [re.compile(pattern, re.IGNORECASE) for pattern in self.malicious_patterns]
|
||||
|
||||
# Max file sizes (in bytes)
|
||||
self.max_video_size = 2 * 1024 * 1024 * 1024 # 2GB
|
||||
# Max file sizes (in bytes) — sourced from central config (T-14)
|
||||
self.max_video_size = settings.upload_max_video_bytes
|
||||
self.max_subtitle_size = 10 * 1024 * 1024 # 10MB
|
||||
|
||||
# Request size limits
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ class NotifyClientTask(Task):
|
|||
if "captions_vtt_gcs" in lang_output:
|
||||
blob_path = lang_output["captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
|
||||
try:
|
||||
signed_url = await get_signed_download_url(blob_path, 24)
|
||||
signed_url = await get_signed_download_url(blob_path, settings.upload_signed_url_ttl_hours)
|
||||
lang_downloads["captions_vtt"] = signed_url
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to generate signed URL for captions {language}: {e}")
|
||||
|
|
@ -89,7 +89,7 @@ class NotifyClientTask(Task):
|
|||
if "ad_vtt_gcs" in lang_output:
|
||||
blob_path = lang_output["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
|
||||
try:
|
||||
signed_url = await get_signed_download_url(blob_path, 24)
|
||||
signed_url = await get_signed_download_url(blob_path, settings.upload_signed_url_ttl_hours)
|
||||
lang_downloads["audio_description_vtt"] = signed_url
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to generate signed URL for AD VTT {language}: {e}")
|
||||
|
|
@ -98,7 +98,7 @@ class NotifyClientTask(Task):
|
|||
if "ad_mp3_gcs" in lang_output:
|
||||
blob_path = lang_output["ad_mp3_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
|
||||
try:
|
||||
signed_url = await get_signed_download_url(blob_path, 24)
|
||||
signed_url = await get_signed_download_url(blob_path, settings.upload_signed_url_ttl_hours)
|
||||
lang_downloads["audio_description_mp3"] = signed_url
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to generate signed URL for AD MP3 {language}: {e}")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { JobStatus } from '../types/api';
|
||||
import { getJobStatusLabel } from '../utils/jobStatusMessages';
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: JobStatus;
|
||||
|
|
@ -37,40 +38,7 @@ export function StatusBadge({ status }: StatusBadgeProps) {
|
|||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: JobStatus) => {
|
||||
switch (status) {
|
||||
case 'created':
|
||||
return 'Created';
|
||||
case 'ingesting':
|
||||
return 'Ingesting';
|
||||
case 'ai_processing':
|
||||
return 'AI Processing';
|
||||
case 'pending_qc':
|
||||
return 'Pending QC';
|
||||
case 'approved_english':
|
||||
return 'Approved (EN)';
|
||||
case 'approved_source':
|
||||
return 'Approved for Translation';
|
||||
case 'rejected':
|
||||
return 'Rejected';
|
||||
case 'tts_failed':
|
||||
return 'TTS Failed';
|
||||
case 'render_failed':
|
||||
return 'Render Failed';
|
||||
case 'translating':
|
||||
return 'Translating';
|
||||
case 'tts_generating':
|
||||
return 'Generating Audio';
|
||||
case 'rendering_video':
|
||||
return 'Rendering Video';
|
||||
case 'pending_final_review':
|
||||
return 'Pending Final Review';
|
||||
case 'completed':
|
||||
return 'Completed';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
const getStatusLabel = getJobStatusLabel;
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusStyles(status)}`}>
|
||||
|
|
|
|||
|
|
@ -335,23 +335,27 @@ export function QCDetail() {
|
|||
|
||||
switch (event.key.toLowerCase()) {
|
||||
case 'a':
|
||||
if (event.ctrlKey || event.metaKey) return; // Don't interfere with Ctrl+A
|
||||
if (event.ctrlKey || event.metaKey) return;
|
||||
event.preventDefault();
|
||||
if (!isProcessing) {
|
||||
handleApprove();
|
||||
}
|
||||
if (!isProcessing) handleApprove();
|
||||
break;
|
||||
case 'r':
|
||||
if (event.ctrlKey || event.metaKey) return; // Don't interfere with Ctrl+R
|
||||
if (event.ctrlKey || event.metaKey) return;
|
||||
event.preventDefault();
|
||||
if (!isProcessing && !showRejectForm) {
|
||||
setShowRejectForm(true);
|
||||
if (!isProcessing && !showRejectForm) setShowRejectForm(true);
|
||||
break;
|
||||
case 's':
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
// Cmd/Ctrl+S — save current VTT (full file)
|
||||
event.preventDefault();
|
||||
if (!isProcessing && !updateVttMutation.isPending) {
|
||||
handleSaveFullVtt();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'escape':
|
||||
if (showRejectForm) {
|
||||
setShowRejectForm(false);
|
||||
}
|
||||
if (showRejectForm) setShowRejectForm(false);
|
||||
if (showLangRejectModal) { setShowLangRejectModal(false); setLangRejectNotes(''); setLangRejectCategory(''); }
|
||||
break;
|
||||
case '1':
|
||||
setViewMode('side-by-side');
|
||||
|
|
@ -367,7 +371,7 @@ export function QCDetail() {
|
|||
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
return () => document.removeEventListener('keydown', handleKeyPress);
|
||||
}, [isProcessing, showRejectForm]);
|
||||
}, [isProcessing, showRejectForm, showLangRejectModal, updateVttMutation.isPending]);
|
||||
|
||||
const handleCaptionsChange = (content: string) => {
|
||||
setCaptionsVtt(content);
|
||||
|
|
@ -407,6 +411,24 @@ export function QCDetail() {
|
|||
}
|
||||
};
|
||||
|
||||
// Cmd+S full-VTT save (hotkey handler)
|
||||
const handleSaveFullVtt = async () => {
|
||||
if (!id || (!captionsVtt && !adVtt)) return;
|
||||
try {
|
||||
await updateVttMutation.mutateAsync({
|
||||
id,
|
||||
data: {
|
||||
captions_vtt: captionsVtt || undefined,
|
||||
audio_description_vtt: adVtt || undefined,
|
||||
language: selectedLanguage,
|
||||
}
|
||||
});
|
||||
toast.toastOnly.success('VTT saved');
|
||||
} catch {
|
||||
toast.toastOnly.error('Failed to save VTT');
|
||||
}
|
||||
};
|
||||
|
||||
// Immediate save handlers for individual cue edits
|
||||
const handleCaptionsCueSave = async (cueIndex: number, vttContent: string) => {
|
||||
if (!id) return;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useState, useMemo, useEffect } from 'react';
|
|||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { formatDistanceToNow, subDays, isAfter, isPast, format } from 'date-fns';
|
||||
import { useAuthStore } from '../../lib/auth';
|
||||
import { JOB_STATUS_LABELS, getJobStatusLabel } from '../../utils/jobStatusMessages';
|
||||
import { useJobs, useBulkDeleteJobs, useReprocessJob, useBulkReturnToQC, useCloneJob } from '../../hooks/useJob';
|
||||
import { StatusBadge } from '../../components/StatusBadge';
|
||||
import { useToastContext } from '../../contexts/ToastContext';
|
||||
|
|
@ -18,26 +19,8 @@ const RETURN_TO_QC_ELIGIBLE_STATUSES = [
|
|||
'tts_failed', 'render_failed', 'approved_english', 'approved_source',
|
||||
];
|
||||
|
||||
// Human-readable status labels
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
'created': 'Created',
|
||||
'ingesting': 'Ingesting',
|
||||
'ai_processing': 'AI Processing',
|
||||
'pending_qc': 'Pending QC',
|
||||
'approved_english': 'Approved (EN)',
|
||||
'approved_source': 'Approved for Translation',
|
||||
'rejected': 'Rejected',
|
||||
'qc_feedback': 'QC Feedback',
|
||||
'translating': 'Translating',
|
||||
'tts_generating': 'Generating Audio',
|
||||
'tts_failed': 'TTS Failed',
|
||||
'rendering_video': 'Rendering Video',
|
||||
'render_failed': 'Render Failed',
|
||||
'pending_final_review': 'Pending Final Review',
|
||||
'completed': 'Completed',
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string): string => STATUS_LABELS[status] || status;
|
||||
// Status labels imported from shared util (T-1)
|
||||
const getStatusLabel = getJobStatusLabel;
|
||||
|
||||
// Parse date string as UTC (backend returns UTC dates without timezone indicator)
|
||||
const parseUTCDate = (dateString: string): Date => {
|
||||
|
|
@ -154,7 +137,7 @@ export function JobsList() {
|
|||
}, [jobsData?.jobs]);
|
||||
|
||||
// All possible statuses for the dropdown (static, server-side filtered)
|
||||
const uniqueStatuses = Object.keys(STATUS_LABELS);
|
||||
const uniqueStatuses = Object.keys(JOB_STATUS_LABELS);
|
||||
|
||||
// Handle column sort
|
||||
const handleSort = (column: string) => {
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export function getStatusMessageConfig(
|
|||
*/
|
||||
export function getProgressMessage(status: string, progress?: number): string {
|
||||
const progressText = progress !== undefined ? ` (${progress}%)` : '';
|
||||
|
||||
|
||||
switch (status) {
|
||||
case 'ingesting':
|
||||
return `Ingesting video${progressText}`;
|
||||
|
|
@ -141,4 +141,26 @@ export function getProgressMessage(status: string, progress?: number): string {
|
|||
default:
|
||||
return status.replace(/_/g, ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Single source of truth for human-readable job status labels. */
|
||||
export const JOB_STATUS_LABELS: Record<string, string> = {
|
||||
created: 'Created',
|
||||
ingesting: 'Ingesting',
|
||||
ai_processing: 'AI Processing',
|
||||
pending_qc: 'Pending QC',
|
||||
approved_english: 'Approved (EN)',
|
||||
approved_source: 'Approved for Translation',
|
||||
rejected: 'Rejected',
|
||||
qc_feedback: 'QC Feedback',
|
||||
translating: 'Translating',
|
||||
tts_generating: 'Generating Audio',
|
||||
tts_failed: 'TTS Failed',
|
||||
rendering_video: 'Rendering Video',
|
||||
render_failed: 'Render Failed',
|
||||
pending_final_review: 'Pending Final Review',
|
||||
completed: 'Completed',
|
||||
};
|
||||
|
||||
export const getJobStatusLabel = (status: string): string =>
|
||||
JOB_STATUS_LABELS[status] ?? status.replace(/_/g, ' ');
|
||||
Loading…
Add table
Reference in a new issue