OrganiLAB 1.6.1: organizador, actualizador, interfaz 1.6 y publicación
Primer commit del proyecto. Incluye el rediseño 1.6 (riel de pasos en la barra lateral), el actualizador con reintentos y respaldo, publicar.sh y la nueva opción de desconectar carpetas (1.6.1). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
559
reglas.py
Archivo normal
559
reglas.py
Archivo normal
@@ -0,0 +1,559 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Reglas de clasificación del material de Labstream.
|
||||
|
||||
Aquí vive todo lo que "sabe" el organizador: cómo reconocer de qué cámara
|
||||
viene un archivo, qué es una carpeta por su nombre, qué es basura y la
|
||||
estructura canónica a la que se lleva cada cosa según el tipo de cliente.
|
||||
|
||||
El diccionario de carpetas se siembra desde aquí la primera vez y después
|
||||
se guarda en datos/diccionario.json, donde la app va aprendiendo.
|
||||
"""
|
||||
import re
|
||||
import unicodedata
|
||||
import datetime
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Normalización de nombres
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def norm(s: str) -> str:
|
||||
"""minúsculas, sin acentos, separadores colapsados a un espacio"""
|
||||
s = unicodedata.normalize("NFKD", s or "")
|
||||
s = "".join(c for c in s if not unicodedata.combining(c))
|
||||
s = s.lower()
|
||||
s = re.sub(r"[\s_\-\.:,;()\[\]{}]+", " ", s)
|
||||
return s.strip()
|
||||
|
||||
|
||||
MESES = ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio",
|
||||
"agosto", "septiembre", "octubre", "noviembre", "diciembre"]
|
||||
MESES_TITULO = [m.capitalize() for m in MESES]
|
||||
MESES_ALIAS = {
|
||||
"setiembre": "septiembre", "septiempre": "septiembre", "sept": "septiembre",
|
||||
"sep": "septiembre", "ene": "enero", "feb": "febrero", "mar": "marzo",
|
||||
"abr": "abril", "may": "mayo", "jun": "junio", "jul": "julio", "ago": "agosto",
|
||||
"oct": "octubre", "nov": "noviembre", "dic": "diciembre",
|
||||
"january": "enero", "february": "febrero", "march": "marzo", "april": "abril",
|
||||
"june": "junio", "july": "julio", "august": "agosto", "september": "septiembre",
|
||||
"october": "octubre", "november": "noviembre", "december": "diciembre",
|
||||
}
|
||||
|
||||
|
||||
def detectar_anio(nombre: str):
|
||||
m = re.fullmatch(r"\s*(20\d\d)\s*", nombre or "")
|
||||
return int(m.group(1)) if m else None
|
||||
|
||||
|
||||
def detectar_mes(nombre: str):
|
||||
"""Devuelve (num_mes, conflicto) si la carpeta ES un mes ("09- Septiembre",
|
||||
"02-marzo", "MAYO", "05"). conflicto=True cuando el número no coincide con
|
||||
el nombre (gana el nombre)."""
|
||||
n = norm(nombre)
|
||||
m = re.fullmatch(r"(\d{1,2})?\s*([a-z]*)", n)
|
||||
if not m:
|
||||
return None, False
|
||||
num = int(m.group(1)) if m.group(1) else None
|
||||
palabra = m.group(2)
|
||||
palabra = MESES_ALIAS.get(palabra, palabra)
|
||||
mes_por_nombre = MESES.index(palabra) + 1 if palabra in MESES else None
|
||||
if mes_por_nombre:
|
||||
return mes_por_nombre, (num is not None and num != mes_por_nombre)
|
||||
if num and 1 <= num <= 12 and not palabra:
|
||||
return num, True # «3» solo puede ser marzo… o la pieza número 3: se propone con confianza media, no alta
|
||||
return None, False
|
||||
|
||||
|
||||
def nombre_mes(mes: int) -> str:
|
||||
"""convención de la casa: «MM- Mes» (la forma más usada en las carpetas actuales)"""
|
||||
return f"{mes:02d}- {MESES_TITULO[mes - 1]}"
|
||||
|
||||
|
||||
def detectar_pieza(nombre: str):
|
||||
"""'01- Exosomas' -> (1, 'Exosomas'); None si no está numerada."""
|
||||
if detectar_anio(nombre) or detectar_mes(nombre)[0]:
|
||||
return None
|
||||
m = re.match(r"^\s*(\d{1,2})\s*[-_.:]+\s*(.+?)\s*$", nombre or "")
|
||||
if not m:
|
||||
return None
|
||||
num, titulo = int(m.group(1)), m.group(2)
|
||||
if num == 0:
|
||||
return None # "00_Grabacion" es estructura, no una pieza
|
||||
titulo = re.sub(r"\s{2,}", " ", titulo).strip(" -_")
|
||||
return num, titulo
|
||||
|
||||
|
||||
def nombre_pieza(num: int, titulo: str) -> str:
|
||||
"""convención de la casa: «NN- Título» (la forma más usada en las carpetas actuales)"""
|
||||
return f"{num:02d}- {titulo}"
|
||||
|
||||
|
||||
_RX_ENVATO_DIR = re.compile(r"-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-utc$", re.I)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Firmas de archivos: de qué cámara / herramienta viene
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
FIRMAS = [
|
||||
("URSA_BRAW", r"^[A-Z]\d{3}_\d{8}_C\d{3}(_\d+)?\.braw$", "Blackmagic (BRAW)"),
|
||||
("BM_PROXY", r"^[A-Z]\d{3}_\d{8}_C\d{3}(_\d+)?\.(mp4|mov)$", "Blackmagic proxy H.264 / ProRes"),
|
||||
("SONY", r"^\d{8}_labstream\d{4}(m01)?\.(mp4|xml)$", "Sony XAVC"),
|
||||
("SONY", r"^c\d{4}(m01)?\.(mp4|mov|xml)$", "Sony"),
|
||||
("OSMO", r"^dji_\d{14}_\d{4}_d\.(mp4|mov|lrf)$", "DJI Osmo Pocket"),
|
||||
("DJI", r"^dji_\d{4}\.(mp4|mov|jpg|dng|lrf|srt)$", "DJI dron / Osmo"),
|
||||
("DJI_MIC", r"^dji_\d{2}_\d{8}_\d{6}\.wav$", "DJI Mic"),
|
||||
("TASCAM", r"^tascam_\d{4}\.wav$", "Tascam"),
|
||||
("ATOMOS", r"^ninjav_", "Atomos Ninja V"),
|
||||
("CELULAR", r"^img_\d{4}( \(\d+\))?( \d)?(_[a-z0-9]+)?\.(mov|mp4|heic|jpg|jpeg|png)$", "iPhone"),
|
||||
("CELULAR", r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(mov|heic|jpg|jpeg|mp4|png)$", "iPhone (AirDrop)"),
|
||||
("CELULAR", r"^(vid|pxl)_\d{8}", "Android"),
|
||||
("WHATSAPP", r"^whatsapp (image|video|audio) ", "WhatsApp"),
|
||||
("PANASONIC", r"^_?\d{7}\.(rw2|jpg)$", "Panasonic Lumix"),
|
||||
("SONY_FOTO", r"^dsc\d{5}\.(arw|jpg)$", "Sony foto"),
|
||||
("IA_HIGGSFIELD", r"^hf_\d{8}_\d{6}_", "Higgsfield"),
|
||||
("IA_CHATGPT", r"^chatgpt image ", "ChatGPT"),
|
||||
("IA_VIDEO", r"^(animate_this_image|video_\d|kling|runway|veo_|sora)", "IA video"),
|
||||
("IA_VOZ", r"^elevenlabs_", "ElevenLabs"),
|
||||
("STOCK", r"-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-utc(\b|[ .(])", "Envato Elements"),
|
||||
("CAPTURA", r"^(captura de pantalla|grabaci[oó]n de pantalla|screenshot|screen recording|grabaci[oó]n \d{4})", "Captura de pantalla"),
|
||||
]
|
||||
FIRMAS_RE = [(k, re.compile(p, re.I), d) for k, p, d in FIRMAS]
|
||||
|
||||
FUENTE_CARPETA = { # nombre de subcarpeta al separar una sesión de grabación por fuente
|
||||
"URSA_BRAW": "URSA_BRAW", "BM_PROXY": "BLACKMAGIC_MP4", "SONY": "SONY", "OSMO": "OSMO",
|
||||
"DJI": "DJI", "DJI_MIC": "AUDIO", "TASCAM": "AUDIO", "ATOMOS": "ATOMOS",
|
||||
"CELULAR": "CELULAR", "WHATSAPP": "CELULAR", "PANASONIC": "FOTOS", "SONY_FOTO": "FOTOS",
|
||||
}
|
||||
|
||||
|
||||
def firma_archivo(nombre: str):
|
||||
for k, rx, desc in FIRMAS_RE:
|
||||
if rx.search(nombre):
|
||||
return k
|
||||
return None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Extensiones
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
EXT = {
|
||||
"video": {".mov", ".mp4", ".m4v", ".mxf", ".mts", ".avi", ".mkv", ".webm", ".braw", ".r3d", ".m2ts"},
|
||||
"video_raw": {".braw", ".r3d", ".ari"},
|
||||
"audio": {".wav", ".mp3", ".aac", ".m4a", ".aif", ".aiff", ".flac", ".ogg", ".wma"},
|
||||
"imagen": {".jpg", ".jpeg", ".png", ".heic", ".webp", ".gif", ".tif", ".tiff", ".bmp", ".svg"},
|
||||
"foto_raw": {".arw", ".rw2", ".dng", ".cr2", ".cr3", ".nef", ".raf", ".xmp"},
|
||||
"diseno": {".psd", ".psb", ".ai", ".af", ".afphoto", ".afdesign", ".indd", ".xd", ".fig", ".sketch", ".eps"},
|
||||
"proyecto": {".drp", ".drb", ".drt", ".dra", ".prproj", ".aep", ".prin", ".fcpxml", ".aaf", ".edl", ".otio", ".xml", ".comp", ".setting", ".mogrt", ".drfx"},
|
||||
"documento": {".pdf", ".docx", ".doc", ".md", ".txt", ".pptx", ".xlsx", ".csv", ".rtf", ".ttml", ".srt", ".vtt", ".pages", ".key", ".numbers"},
|
||||
"fuente": {".ttf", ".otf", ".woff", ".woff2"},
|
||||
"comprimido": {".zip", ".rar", ".7z", ".tar", ".gz", ".dmg", ".iso", ".img"},
|
||||
"lut": {".cube", ".3dl", ".look"},
|
||||
"programa": {".exe", ".msi", ".pkg", ".app", ".lnk"},
|
||||
}
|
||||
BASURA_NOMBRES = {".ds_store", "thumbs.db", "desktop.ini", ".localized", ".bridgesort", ".bridgelabelsandratings"}
|
||||
BASURA_EXT = {".tmp", ".crdownload", ".part", ".pek", ".cfa", ".sfk", ".bak", ".ims", ".idx", ".dat"}
|
||||
BASURA_DIRS = {"__macosx", "#recycle", ".synologyworkingdirectory", "@eadir", "media cache", "media cache files", "peak files"}
|
||||
BASURA_DIRS_CONTIENE = ["almacenamiento automatico", "auto save", "autosave", "auto-save", "adobe premiere pro auto"]
|
||||
PROXY_DIRS = {"proxy", "proxies", "proxy media", "proxytest", "_proxytest", "optimized media", "cache"}
|
||||
|
||||
|
||||
def es_basura_archivo(nombre: str) -> bool:
|
||||
n = nombre.lower()
|
||||
if n in BASURA_NOMBRES or n.startswith("._"):
|
||||
return True
|
||||
if n.endswith("~lock~") or n.endswith("~"):
|
||||
return True
|
||||
ext = n[n.rfind("."):] if "." in n else ""
|
||||
if ext in BASURA_EXT:
|
||||
return True
|
||||
if re.match(r"^ps[a-z0-9]{3,5}\.tmp$", n) or re.match(r"^adobe photoshop \d{10,}$", n):
|
||||
return True
|
||||
if re.match(r"^[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}\.tmp$", n):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def es_basura_dir(nombre: str) -> bool:
|
||||
n = norm(nombre)
|
||||
if n in BASURA_DIRS or nombre.lower() in BASURA_DIRS:
|
||||
return True
|
||||
return any(t in n for t in BASURA_DIRS_CONTIENE)
|
||||
|
||||
|
||||
def es_proxy_dir(nombre: str) -> bool:
|
||||
return norm(nombre) in PROXY_DIRS or nombre.lower() in {".proxy"}
|
||||
|
||||
|
||||
def rol_archivo(nombre: str, ext: str, firma: str | None) -> str:
|
||||
"""Qué es el archivo por sí mismo (sin mirar la carpeta)."""
|
||||
e = ext.lower()
|
||||
if es_basura_archivo(nombre):
|
||||
return "basura"
|
||||
if firma in ("URSA_BRAW",) or e in EXT["video_raw"]:
|
||||
return "camara_raw"
|
||||
if firma in ("BM_PROXY", "SONY", "OSMO", "DJI", "ATOMOS") and e in EXT["video"]:
|
||||
return "camara_video"
|
||||
if firma == "SONY" and e == ".xml":
|
||||
return "camara_sidecar"
|
||||
if firma in ("CELULAR", "WHATSAPP") and e in EXT["video"]:
|
||||
return "celular_video"
|
||||
if firma in ("CELULAR", "WHATSAPP") and e in EXT["imagen"]:
|
||||
return "celular_foto"
|
||||
if firma in ("DJI_MIC", "TASCAM"):
|
||||
return "audio_grabado"
|
||||
if firma in ("PANASONIC", "SONY_FOTO") or e in EXT["foto_raw"]:
|
||||
return "foto_camara"
|
||||
if firma and firma.startswith("IA_"):
|
||||
return "ia"
|
||||
if firma == "STOCK":
|
||||
return "stock"
|
||||
if firma == "CAPTURA":
|
||||
return "captura"
|
||||
if e == ".lrf":
|
||||
return "proxy"
|
||||
if e in EXT["video"]:
|
||||
return "video"
|
||||
if e in EXT["audio"]:
|
||||
return "audio"
|
||||
if e in EXT["imagen"]:
|
||||
return "imagen"
|
||||
if e in EXT["diseno"]:
|
||||
return "diseno"
|
||||
if e in EXT["proyecto"]:
|
||||
return "proyecto"
|
||||
if e in EXT["documento"]:
|
||||
return "documento"
|
||||
if e in EXT["fuente"]:
|
||||
return "fuente"
|
||||
if e in EXT["comprimido"]:
|
||||
return "comprimido"
|
||||
if e in EXT["lut"]:
|
||||
return "lut"
|
||||
if e in EXT["programa"]:
|
||||
return "programa"
|
||||
return "otro"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Fecha a partir del nombre del archivo
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
_RX_YMD = re.compile(r"(?<!\d)(20\d{2})(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])(?!\d)")
|
||||
_RX_YMD_HMS = re.compile(r"(?<!\d)(20\d{2})(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{6}(?!\d)")
|
||||
_RX_Y_M_D = re.compile(r"(?<!\d)(20\d{2})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])(?!\d)")
|
||||
_RX_ES = re.compile(r"(\d{1,2}) (ene|feb|mar|abr|may|jun|jul|ago|sep|oct|nov|dic)\w*\.? (20\d{2})", re.I)
|
||||
_RX_BM = re.compile(r"^[A-Z]\d{3}_(\d{2})(\d{2})\d{4}_C\d{3}", re.I)
|
||||
_RX_WA = re.compile(r"WhatsApp \w+ (20\d{2})-(\d{2})-(\d{2})", re.I)
|
||||
|
||||
|
||||
def fecha_de_nombre(nombre: str, mtime: float):
|
||||
"""(YYYY-MM-DD, origen) — origen 'nombre' si sale del nombre, 'mtime' si no."""
|
||||
for rx in (_RX_YMD, _RX_YMD_HMS, _RX_Y_M_D, _RX_WA):
|
||||
m = rx.search(nombre)
|
||||
if m:
|
||||
try:
|
||||
d = datetime.date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
||||
return d.isoformat(), "nombre"
|
||||
except ValueError:
|
||||
pass
|
||||
m = _RX_ES.search(nombre)
|
||||
if m:
|
||||
mes = ["ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"].index(m.group(2).lower()[:3]) + 1
|
||||
try:
|
||||
return datetime.date(int(m.group(3)), mes, int(m.group(1))).isoformat(), "nombre"
|
||||
except ValueError:
|
||||
pass
|
||||
m = _RX_BM.match(nombre)
|
||||
if m:
|
||||
mes, dia = int(m.group(1)), int(m.group(2))
|
||||
if 1 <= mes <= 12 and 1 <= dia <= 31:
|
||||
mt = datetime.datetime.fromtimestamp(mtime)
|
||||
anio = mt.year if (mes, dia) <= (mt.month, mt.day) else mt.year - 1
|
||||
try:
|
||||
return datetime.date(anio, mes, dia).isoformat(), "nombre+mtime"
|
||||
except ValueError:
|
||||
pass
|
||||
return datetime.date.fromtimestamp(mtime).isoformat(), "mtime"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Cubetas (buckets): qué es una carpeta
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
CUBETAS = {
|
||||
"GRABACION": "Grabación (originales de cámara de una sesión)",
|
||||
"PIEZA": "Pieza de contenido (un reel, un video)",
|
||||
"CAMPANA": "Campaña de marca / pauta",
|
||||
"EXPORTADOS": "Exportados / masters finales",
|
||||
"PORTADAS": "Portadas y miniaturas",
|
||||
"APOYOS": "Tomas de apoyo reutilizables (b-roll)",
|
||||
"MARCA": "Material de marca (logos, fuentes, fondos, transiciones)",
|
||||
"PROYECTOS": "Proyectos de edición (Resolve, Premiere, AE, Photoshop)",
|
||||
"MUSICA": "Música",
|
||||
"SFX": "Efectos de sonido",
|
||||
"VOZ": "Voces / locuciones",
|
||||
"IA": "Generado con IA",
|
||||
"STOCK": "Stock (Envato, etc.)",
|
||||
"GUION": "Guiones y textos",
|
||||
"FOTOS": "Fotografías",
|
||||
"DOCUMENTOS": "Documentos, propuestas, referentes",
|
||||
"CELULAR": "Material de celular",
|
||||
"PROXY": "Proxies (se pueden regenerar)",
|
||||
"BASURA": "Basura de sistema / cachés",
|
||||
"CONTENEDOR": "Carpeta contenedora: mirar adentro",
|
||||
"REVISAR": "Revisar a mano",
|
||||
"IGNORAR": "Dejar como está",
|
||||
}
|
||||
|
||||
# token normalizado -> cubeta. Se siembra en datos/diccionario.json y de ahí aprende.
|
||||
DICCIONARIO_SEMILLA = {
|
||||
# grabación
|
||||
"grabacion": "GRABACION", "grabaciones": "GRABACION", "grabado": "GRABACION", "grabados": "GRABACION",
|
||||
"tomas": "GRABACION", "rushes": "GRABACION", "brutos": "GRABACION", "raw": "GRABACION",
|
||||
"ursa": "GRABACION", "braw": "GRABACION", "sony": "GRABACION", "osmo": "GRABACION", "dji": "GRABACION", "blackmagic": "GRABACION",
|
||||
"blackmagic braw": "GRABACION", "otros blackmagic": "GRABACION", "otro sony": "GRABACION", "dji audio": "GRABACION",
|
||||
"tascam": "GRABACION", "camara": "GRABACION", "videos originales": "GRABACION", "originales": "GRABACION",
|
||||
# exportados
|
||||
"render": "EXPORTADOS", "renders": "EXPORTADOS", "export": "EXPORTADOS", "exportado": "EXPORTADOS",
|
||||
"exportados": "EXPORTADOS", "exports": "EXPORTADOS", "finales": "EXPORTADOS", "final": "EXPORTADOS",
|
||||
"videos finales": "EXPORTADOS", "videos finales pantalla": "EXPORTADOS", "finales enviar": "EXPORTADOS",
|
||||
"entrega": "EXPORTADOS", "entregas": "EXPORTADOS", "master": "EXPORTADOS", "masters": "EXPORTADOS",
|
||||
"entregable": "EXPORTADOS", "entregables": "EXPORTADOS", "publicados": "EXPORTADOS",
|
||||
# portadas
|
||||
"portada": "PORTADAS", "portadas": "PORTADAS", "miniatura": "PORTADAS", "miniaturas": "PORTADAS",
|
||||
"thumbnails": "PORTADAS", "thumbnail": "PORTADAS", "covers": "PORTADAS", "cover": "PORTADAS",
|
||||
# marca
|
||||
"material de marca": "MARCA", "marca": "MARCA", "manual de marca": "MARCA", "branding": "MARCA",
|
||||
"logos": "MARCA", "logo": "MARCA", "logos png": "MARCA", "fuente": "MARCA", "fuentes": "MARCA",
|
||||
"tipografia": "MARCA", "tipografias": "MARCA", "fondo": "MARCA", "fondos": "MARCA",
|
||||
"transitions": "MARCA", "transiciones": "MARCA", "efectos": "MARCA", "texto": "MARCA", "textos": "MARCA",
|
||||
"intro": "MARCA", "intros": "MARCA", "plantillas": "MARCA", "luts": "MARCA",
|
||||
# proyectos
|
||||
"adobe": "PROYECTOS", "proyecto": "PROYECTOS", "proyectos": "PROYECTOS", "premiere": "PROYECTOS",
|
||||
"resolve": "PROYECTOS", "after": "PROYECTOS", "after effects": "PROYECTOS", "photoshop": "PROYECTOS",
|
||||
"animaciones": "PROYECTOS", "prerenders": "PROYECTOS", "socalo": "PROYECTOS",
|
||||
# música / sonido / voz
|
||||
"musica": "MUSICA", "music": "MUSICA", "canciones": "MUSICA", "soundtrack": "MUSICA", "julio music": "MUSICA",
|
||||
"sonidos": "SFX", "sfx": "SFX", "swoosh": "SFX", "whoosh": "SFX", "efectos de sonido": "SFX",
|
||||
"voces": "VOZ", "voz": "VOZ", "elevenlabs": "VOZ", "locucion": "VOZ", "locuciones": "VOZ", "narracion": "VOZ",
|
||||
"audio": "VOZ", "audios": "VOZ",
|
||||
# ia
|
||||
"ia": "IA", "ai": "IA", "imagenes ia": "IA", "imagenes ia importante": "IA", "avatars": "IA", "avatares": "IA",
|
||||
"higgsfield": "IA", "chatgpt": "IA", "generado": "IA",
|
||||
# stock
|
||||
"stock": "STOCK", "envato": "STOCK", "recursos": "STOCK", "elements": "STOCK", "recursos audiovisuales": "STOCK",
|
||||
# guion / documentos
|
||||
"guion": "GUION", "guiones": "GUION", "script": "GUION", "scripts": "GUION",
|
||||
"propuesta": "DOCUMENTOS", "propuestas": "DOCUMENTOS", "referentes": "DOCUMENTOS", "referencia": "DOCUMENTOS",
|
||||
"referencias": "DOCUMENTOS", "imagen referencia": "DOCUMENTOS", "brief": "DOCUMENTOS", "documentos": "DOCUMENTOS",
|
||||
"claude": "DOCUMENTOS", "conferencia": "DOCUMENTOS",
|
||||
# fotos
|
||||
"fotos": "FOTOS", "fotografias": "FOTOS", "fotografia": "FOTOS", "jpeg": "FOTOS", "imagenes": "FOTOS", "imagen": "FOTOS",
|
||||
# apoyos
|
||||
"tomas de apoyo": "APOYOS", "tomas apoyos": "APOYOS", "apoyos": "APOYOS", "apoyo": "APOYOS", "b roll": "APOYOS",
|
||||
"broll": "APOYOS", "rellenos": "APOYOS",
|
||||
# campañas de marca
|
||||
"dove": "CAMPANA", "loreal": "CAMPANA", "garnier": "CAMPANA", "isdin": "CAMPANA", "cetaphil": "CAMPANA",
|
||||
"artemis": "CAMPANA", "vinicius": "CAMPANA", "the organic pharmacy": "CAMPANA", "nixoderm": "CAMPANA",
|
||||
"publicidad": "CAMPANA", "pautas facebook": "CAMPANA", "pauta": "CAMPANA", "campana": "CAMPANA",
|
||||
"campanas": "CAMPANA", "anuncio": "CAMPANA", "ads": "CAMPANA", "vsl": "CAMPANA",
|
||||
"vsls": "CAMPANA", "lanzamientos": "CAMPANA", "tanda": "CAMPANA",
|
||||
# celular
|
||||
"celular": "CELULAR", "material celular": "CELULAR", "iphone": "CELULAR", "calular pero de": "CELULAR",
|
||||
# proxies y basura
|
||||
"proxy": "PROXY", "proxies": "PROXY", "macosx": "BASURA", "recycle": "BASURA",
|
||||
# contenedores: hay que mirar adentro
|
||||
"nuevo": "CONTENEDOR", "nuevo orden": "CONTENEDOR", "nuevo 2026": "CONTENEDOR", "anuncios": "CONTENEDOR",
|
||||
"material": "IGNORAR", # "Material" es estructura (de una campaña o de la raíz de pauta): no se mueve
|
||||
"otros videos": "CONTENEDOR", "otros": "CONTENEDOR", "varios": "CONTENEDOR", "sin fecha": "CONTENEDOR",
|
||||
"temporal": "CONTENEDOR", "temp": "CONTENEDOR", "desordenado": "CONTENEDOR", "desordenado revisar": "CONTENEDOR",
|
||||
"sacado de nubes": "CONTENEDOR", "cambio de proyecto": "CONTENEDOR", "nueva carpeta": "CONTENEDOR",
|
||||
"repetidos": "IGNORAR", "duplicados": "IGNORAR", "backup": "REVISAR", "respaldo": "REVISAR",
|
||||
# cosas del cliente que son piezas o series
|
||||
"reels": "PIEZA", "reel": "PIEZA", "historia": "PIEZA", "historias": "PIEZA", "carrusel": "PIEZA",
|
||||
"podcast": "PIEZA", "consultorio": "PIEZA", "testimonios": "PIEZA", "clases": "PIEZA",
|
||||
# subcarpetas normales DENTRO de una pieza: se dejan como están
|
||||
"visual": "IGNORAR", "visula": "IGNORAR", "01 visual": "IGNORAR", "chroma key": "IGNORAR",
|
||||
"prerenders": "IGNORAR", "elements": "STOCK", "elemnts": "STOCK", "stems": "MUSICA", "loops": "MUSICA",
|
||||
"shorts": "MUSICA", "full": "MUSICA", "exportada": "EXPORTADOS", "higgsfiel": "IA",
|
||||
}
|
||||
|
||||
# sub-carpeta canónica dentro de 00_Marca
|
||||
MARCA_SUB = {
|
||||
"fuente": "Fuentes", "fuentes": "Fuentes", "tipografia": "Fuentes", "tipografias": "Fuentes",
|
||||
"fondo": "Fondos", "fondos": "Fondos", "transitions": "Transiciones", "transiciones": "Transiciones",
|
||||
"efectos": "Transiciones", "sonidos": "Sonidos", "texto": "Textos", "textos": "Textos",
|
||||
"logos": "Logos", "logo": "Logos", "logos png": "Logos", "manual de marca": "Manual", "intro": "Intros",
|
||||
"intros": "Intros", "luts": "LUTs", "plantillas": "Plantillas",
|
||||
}
|
||||
|
||||
|
||||
def cubeta_carpeta(nombre: str, diccionario: dict):
|
||||
"""(cubeta, motivo) de una carpeta por su nombre; None si no se reconoce."""
|
||||
if es_basura_dir(nombre):
|
||||
return "BASURA", "basura de sistema"
|
||||
if es_proxy_dir(nombre):
|
||||
return "PROXY", "carpeta de proxies"
|
||||
if _RX_ENVATO_DIR.search(nombre.strip()):
|
||||
return "STOCK", "nombre de paquete de Envato"
|
||||
n = norm(nombre)
|
||||
if n in diccionario:
|
||||
return diccionario[n], f"diccionario: «{n}»"
|
||||
# tokens de varias palabras primero, luego palabras sueltas
|
||||
# (CONTENEDOR solo por coincidencia exacta: "Material juan manuel" no es un cajón de sastre)
|
||||
claves = sorted(diccionario.keys(), key=lambda k: -len(k))
|
||||
for k in claves:
|
||||
if " " in k and diccionario[k] != "CONTENEDOR" and re.search(r"(^| )" + re.escape(k) + r"( |$)", n):
|
||||
return diccionario[k], f"diccionario: contiene «{k}»"
|
||||
palabras = n.split()
|
||||
for k in claves:
|
||||
if " " not in k and diccionario[k] != "CONTENEDOR" and k in palabras:
|
||||
return diccionario[k], f"diccionario: palabra «{k}»"
|
||||
return None, ""
|
||||
|
||||
|
||||
# palabras que solo dicen "de qué cámara": no aportan etiqueta a una sesión de grabación
|
||||
FUENTE_PALABRAS = {"grabacion", "grabaciones", "grabado", "grabados", "ursa", "sony", "osmo", "dji", "blackmagic",
|
||||
"braw", "tascam", "audio", "otro", "otros", "a", "c", "camara", "material", "video", "videos",
|
||||
"mp4", "mov", "de", "la", "el", "pocket", "drone", "dron", "mic", "celular"}
|
||||
|
||||
|
||||
def etiqueta_grabacion(nombre: str) -> str:
|
||||
"""'Grabación Loreal Peeling' -> 'Loreal Peeling'; 'Grabación URSA' -> ''"""
|
||||
palabras = [p for p in re.sub(r"\s{2,}", " ", nombre).strip(" -_").split(" ") if p]
|
||||
|
||||
def sobra(w):
|
||||
return w in FUENTE_PALABRAS or w.isdigit() or MESES_ALIAS.get(w, w) in MESES or w in ("-", "_")
|
||||
utiles = [p for p in palabras if not all(sobra(w) for w in norm(p).split()) and norm(p)]
|
||||
if not utiles:
|
||||
return ""
|
||||
return " ".join(utiles)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Plantillas de estructura por tipo de cliente
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
TIPOS_CLIENTE = {
|
||||
"contenido": "Contenido mensual (dermatólogas, marca personal): año / mes / pieza",
|
||||
"pauta": "Pauta y anuncios (campañas por tandas, ganchos, VSL)",
|
||||
"proyecto": "Proyecto puntual corporativo (un trabajo, una entrega)",
|
||||
"interno": "Interno de Labstream (recursos compartidos)",
|
||||
}
|
||||
|
||||
# Destinos relativos a la raíz del cliente. {anio} {mes} = "08-Agosto" {nombre} {fuente}
|
||||
PLANTILLAS = {
|
||||
"contenido": {
|
||||
"MARCA": "00_Marca/{sub}",
|
||||
"PROYECTOS": "01_Proyectos/{nombre}",
|
||||
"GRABACION": "{anio}/{mes}/00_Grabacion/{sub}",
|
||||
"PIEZA": "{anio}/{mes}/{nombre}",
|
||||
"CAMPANA": "Campañas/{anio}_{nombre}",
|
||||
"EXPORTADOS": "{anio}/{mes}/_Exportados/{sub}",
|
||||
"PORTADAS": "{anio}/{mes}/_Portadas/{sub}",
|
||||
"APOYOS": "_Apoyos/{sub}",
|
||||
"MUSICA": "_Recursos/Musica/{sub}",
|
||||
"SFX": "_Recursos/Sonidos/{sub}",
|
||||
"VOZ": "{anio}/{mes}/_Voces/{sub}",
|
||||
"IA": "{anio}/{mes}/_IA/{sub}",
|
||||
"STOCK": "_Recursos/Stock/{sub}",
|
||||
"GUION": "{anio}/{mes}/_Guiones/{sub}",
|
||||
"FOTOS": "{anio}/{mes}/_Fotos/{sub}",
|
||||
"DOCUMENTOS": "_Documentos/{sub}",
|
||||
"CELULAR": "{anio}/{mes}/00_Grabacion/CELULAR/{sub}",
|
||||
"PROXY": "_Para borrar/proxies/{rel}",
|
||||
"BASURA": "_Para borrar/basura/{rel}",
|
||||
"REVISAR": "_Revisar/{nombre}",
|
||||
},
|
||||
"pauta": {
|
||||
"MARCA": "00_Marca/{sub}",
|
||||
"PROYECTOS": "01_Proyectos/{nombre}",
|
||||
"GRABACION": "Material/Grabaciones/{anio}/{mes}/{sub}",
|
||||
"PIEZA": "Campañas/{anio}_{nombre}",
|
||||
"CAMPANA": "Campañas/{anio}_{nombre}",
|
||||
"EXPORTADOS": "_Exportados/{anio}/{sub}",
|
||||
"PORTADAS": "_Portadas/{sub}",
|
||||
"APOYOS": "Material/Apoyos/{sub}",
|
||||
"MUSICA": "_Recursos/Musica/{sub}",
|
||||
"SFX": "_Recursos/Sonidos/{sub}",
|
||||
"VOZ": "Material/Voces/{sub}",
|
||||
"IA": "Material/IA/{sub}",
|
||||
"STOCK": "_Recursos/Stock/{sub}",
|
||||
"GUION": "_Guiones/{sub}",
|
||||
"FOTOS": "Material/Fotos/{sub}",
|
||||
"DOCUMENTOS": "_Documentos/{sub}",
|
||||
"CELULAR": "Material/Celular/{sub}",
|
||||
"PROXY": "_Para borrar/proxies/{rel}",
|
||||
"BASURA": "_Para borrar/basura/{rel}",
|
||||
"REVISAR": "_Revisar/{nombre}",
|
||||
},
|
||||
"proyecto": {
|
||||
"MARCA": "00_Marca/{sub}",
|
||||
"PROYECTOS": "03_Proyecto/{nombre}",
|
||||
"GRABACION": "02_Grabacion/{anio}/{mes}/{sub}",
|
||||
"PIEZA": "04_Exportados/{nombre}",
|
||||
"CAMPANA": "{anio}_{nombre}",
|
||||
"EXPORTADOS": "04_Exportados/{sub}",
|
||||
"PORTADAS": "04_Exportados/Portadas/{sub}",
|
||||
"APOYOS": "01_Material/Apoyos/{sub}",
|
||||
"MUSICA": "01_Material/Musica/{sub}",
|
||||
"SFX": "01_Material/Sonidos/{sub}",
|
||||
"VOZ": "01_Material/Voces/{sub}",
|
||||
"IA": "01_Material/IA/{sub}",
|
||||
"STOCK": "01_Material/Stock/{sub}",
|
||||
"GUION": "00_Brief/{sub}",
|
||||
"FOTOS": "01_Material/Fotos/{sub}",
|
||||
"DOCUMENTOS": "00_Brief/{sub}",
|
||||
"CELULAR": "01_Material/Celular/{sub}",
|
||||
"PROXY": "_Para borrar/proxies/{rel}",
|
||||
"BASURA": "_Para borrar/basura/{rel}",
|
||||
"REVISAR": "_Revisar/{nombre}",
|
||||
},
|
||||
"interno": {
|
||||
"MARCA": "Marca/{sub}",
|
||||
"PROYECTOS": "Proyectos/{nombre}",
|
||||
"GRABACION": "Grabaciones/{anio}/{mes}/{sub}",
|
||||
"PIEZA": "Piezas/{anio}/{nombre}",
|
||||
"CAMPANA": "Campañas/{anio}_{nombre}",
|
||||
"EXPORTADOS": "Exportados/{anio}/{sub}",
|
||||
"PORTADAS": "Portadas/{sub}",
|
||||
"APOYOS": "Apoyos/{sub}",
|
||||
"MUSICA": "Musica/{sub}",
|
||||
"SFX": "Sonidos/{sub}",
|
||||
"VOZ": "Voces/{sub}",
|
||||
"IA": "IA/{sub}",
|
||||
"STOCK": "Stock/{sub}",
|
||||
"GUION": "Guiones/{sub}",
|
||||
"FOTOS": "Fotos/{sub}",
|
||||
"DOCUMENTOS": "Documentos/{sub}",
|
||||
"CELULAR": "Celular/{sub}",
|
||||
"PROXY": "_Para borrar/proxies/{rel}",
|
||||
"BASURA": "_Para borrar/basura/{rel}",
|
||||
"REVISAR": "_Revisar/{nombre}",
|
||||
},
|
||||
}
|
||||
|
||||
# La app NUNCA borra: lo que sobra va a esta carpeta dentro de la misma raíz y una persona la revisa y la borra a mano.
|
||||
PAPELERA = "_Para borrar"
|
||||
PAPELERAS = ("_Para borrar", "_Papelera") # el nombre viejo se sigue reconociendo en carpetas ya ordenadas
|
||||
PAPELERAS_NORM = {norm(x) for x in PAPELERAS}
|
||||
# Los duplicados se juntan en esta subcarpeta AL LADO del archivo que se queda, para revisarlos a mano.
|
||||
DUPLICADOS = "Duplicados"
|
||||
|
||||
# Carpetas que la app crea y que no debe volver a "ordenar" (se comparan ya normalizadas)
|
||||
CARPETAS_PROPIAS = {norm(x) for x in (
|
||||
"_Para borrar", "_Papelera", "Duplicados", "_Revisar", "_Recursos", "_Apoyos", "_Documentos", "00_Marca", "01_Proyectos", "Campañas",
|
||||
"_Exportados", "_Portadas", "00_Grabacion", "_Voces", "_IA", "_Guiones", "_Fotos", "_Musica", "_Archivo",
|
||||
"00_Brief", "01_Material", "02_Grabacion", "03_Proyecto", "04_Exportados")}
|
||||
|
||||
# Rol de archivo suelto -> cubeta (para archivos que están fuera de sitio)
|
||||
ROL_A_CUBETA = {
|
||||
"camara_raw": "GRABACION", "camara_video": "GRABACION", "camara_sidecar": "GRABACION",
|
||||
"audio_grabado": "GRABACION", "celular_video": "CELULAR", "celular_foto": "CELULAR",
|
||||
"foto_camara": "FOTOS", "ia": "IA", "stock": "STOCK", "captura": "REVISAR", "proxy": "PROXY",
|
||||
"video": "EXPORTADOS", "audio": "MUSICA", "imagen": "PORTADAS", "diseno": "PROYECTOS",
|
||||
"proyecto": "PROYECTOS", "documento": "DOCUMENTOS", "fuente": "MARCA", "comprimido": "REVISAR",
|
||||
"lut": "MARCA", "programa": "REVISAR", "otro": "REVISAR", "basura": "BASURA",
|
||||
}
|
||||
Referencia en una nueva incidencia
Block a user