chore(fase0): higiene do repositório + corrige gitignore que escondia fcpxml/models/

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>
This commit is contained in:
João Henrique
2026-09-23 08:28:44 -04:00
co-authored by Claude Sonnet 5
parent 0fdfe33613
commit d13f643ebc
19 changed files with 2013 additions and 29 deletions
+121
View File
@@ -0,0 +1,121 @@
"""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)}"
)