Fase 0 do roteiro de reestruturação (Engine/docs/10_MAPA_REESTRUTURACAO.md): move code/WHISPERX (2,6 GB de backups órfãos, sem uso ativo, sem .gitmodules) para ~/Archives/G-ART-WHISPERX-backup fora do workspace git; traz admin/ para o gate de lint de run_after_fix.sh; corrige fcpxml/writer/adjustment.py, que gerava um wrapper <adjustment> inexistente no DTD 1.13 (filtros agora vão direto no <clip>, na ordem exigida), com teste de regressão novo. Achado à parte: .gitignore tinha uma regra solta "models/" (pensada só para o cache do Whisper em code/models/) que também escondia do git todo o pacote fcpxml/models/ — nunca commitado, sem proteção nenhuma. Corrigida para /code/models/, ancorada na raiz. Docs atualizados no mesmo commit (02_MODULES, 09_MANUTENCAO, 10_MAPA_REESTRUTURACAO, 05_EXPERIENCIAS #34 e #36), conforme a regra do CLAUDE.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
122 lines
3.7 KiB
Python
122 lines
3.7 KiB
Python
"""Achados de QC e o resultado de uma validação.
|
|
|
|
Extraído de models.py — ver fcpxml/models/__init__.py.
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from .enums import FlashFrameSeverity, ValidationIssueType
|
|
from .timing import Timecode
|
|
|
|
|
|
@dataclass
|
|
class FlashFrame:
|
|
"""
|
|
Represents a detected flash frame (ultra-short clip).
|
|
|
|
Flash frames are typically editing errors - clips that are too short
|
|
to be perceived as intentional cuts.
|
|
"""
|
|
clip_name: str
|
|
clip_id: str
|
|
start: Timecode
|
|
duration_frames: int
|
|
duration_seconds: float
|
|
severity: 'FlashFrameSeverity'
|
|
|
|
@property
|
|
def is_critical(self) -> bool:
|
|
"""Check if this is a critical flash frame."""
|
|
return self.severity == FlashFrameSeverity.CRITICAL
|
|
|
|
@dataclass
|
|
class GapInfo:
|
|
"""
|
|
Represents a detected gap in the timeline.
|
|
|
|
Gaps can be intentional (black frames) or errors from deleted clips.
|
|
"""
|
|
start: Timecode
|
|
duration_frames: int
|
|
duration_seconds: float
|
|
previous_clip: Optional[str] = None # Clip name before the gap
|
|
next_clip: Optional[str] = None # Clip name after the gap
|
|
|
|
@property
|
|
def timecode(self) -> str:
|
|
"""Get timecode string for the gap start."""
|
|
return self.start.to_smpte()
|
|
|
|
@dataclass
|
|
class DuplicateGroup:
|
|
"""
|
|
Represents a group of clips using the same source media.
|
|
|
|
Useful for detecting duplicate clips that may be unintentional.
|
|
"""
|
|
source_ref: str # The asset/media reference ID
|
|
source_name: str # Human-readable source name
|
|
clips: List[Dict[str, Any]] = field(default_factory=list) # List of clip info dicts
|
|
|
|
@property
|
|
def count(self) -> int:
|
|
"""Number of clips using this source."""
|
|
return len(self.clips)
|
|
|
|
@property
|
|
def has_overlapping_ranges(self) -> bool:
|
|
"""Check if any clips use overlapping portions of the source."""
|
|
# Sort clips by source_start
|
|
sorted_clips = sorted(self.clips, key=lambda c: c.get('source_start', 0))
|
|
for i in range(len(sorted_clips) - 1):
|
|
curr_end = sorted_clips[i].get('source_start', 0) + sorted_clips[i].get('source_duration', 0)
|
|
next_start = sorted_clips[i + 1].get('source_start', 0)
|
|
if curr_end > next_start:
|
|
return True
|
|
return False
|
|
|
|
@dataclass
|
|
class ValidationIssue:
|
|
"""
|
|
Represents a single validation issue found in a timeline.
|
|
|
|
Used by validate_timeline to report problems.
|
|
"""
|
|
issue_type: 'ValidationIssueType'
|
|
severity: str # "error", "warning", "info"
|
|
message: str
|
|
timecode: Optional[str] = None
|
|
clip_name: Optional[str] = None
|
|
details: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
@dataclass
|
|
class ValidationResult:
|
|
"""
|
|
Result of timeline validation.
|
|
|
|
Provides a health score and categorized list of issues.
|
|
"""
|
|
is_valid: bool
|
|
health_score: int # 0-100 percentage
|
|
issues: List[ValidationIssue] = field(default_factory=list)
|
|
flash_frames: List[FlashFrame] = field(default_factory=list)
|
|
gaps: List[GapInfo] = field(default_factory=list)
|
|
duplicates: List[DuplicateGroup] = field(default_factory=list)
|
|
|
|
@property
|
|
def error_count(self) -> int:
|
|
return len([i for i in self.issues if i.severity == "error"])
|
|
|
|
@property
|
|
def warning_count(self) -> int:
|
|
return len([i for i in self.issues if i.severity == "warning"])
|
|
|
|
def summary(self) -> str:
|
|
"""Generate a summary string."""
|
|
return (
|
|
f"Timeline Health: {self.health_score}% | "
|
|
f"Errors: {self.error_count} | Warnings: {self.warning_count} | "
|
|
f"Flash frames: {len(self.flash_frames)} | Gaps: {len(self.gaps)}"
|
|
)
|