# -*- 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 os 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] # abreviaturas: se entienden, pero nunca con confianza alta (ver MESES_DUDOSOS) 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", } # «Mar», «May» o «Ago» pueden ser un nombre propio, no un mes: se proponen con confianza media MESES_DUDOSOS = {"sep", "ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "oct", "nov", "dic"} 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) abreviada = palabra in MESES_DUDOSOS palabra = MESES_ALIAS.get(palabra, palabra) mes_por_nombre = MESES.index(palabra) + 1 if palabra in MESES else None if mes_por_nombre: # abreviada sin número («Mar») o número que no cuadra con el nombre: confianza media return mes_por_nombre, (num is not None and num != mes_por_nombre) or (abreviada and num is None) 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", ".psdt", ".ai", ".af", ".afphoto", ".afdesign", ".afpub", ".indd", ".idml", ".xd", ".fig", ".sketch", ".eps"}, "proyecto": {".drp", ".drb", ".drt", ".dra", ".prproj", ".prel", ".ppj", ".prtl", ".aep", ".aepx", ".aet", ".prin", ".fcpxml", ".aaf", ".edl", ".otio", ".xml", ".comp", ".setting", ".mogrt", ".drfx", ".sesx", ".c4d", ".blend", ".motn"}, "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"}, } # Qué programa abre cada proyecto. «01_Proyectos» se separa por programa porque en la carpeta de un # cliente conviven .prproj, .aep, .drp y .psd, y lo que cuesta es encontrarlos mezclados. El nombre de # la carpeta es el del programa tal como se dice en el estudio. APPS_PROYECTO = { "Premiere": (".prproj", ".prel", ".ppj", ".prtl", ".prin"), "After Effects": (".aep", ".aepx", ".aet"), "DaVinci Resolve": (".drp", ".drb", ".drt", ".dra", ".drfx", ".setting", ".comp"), "Photoshop": (".psd", ".psb", ".psdt"), "Illustrator": (".ai", ".eps"), "InDesign": (".indd", ".idml"), "Audition": (".sesx",), "Affinity": (".af", ".afphoto", ".afdesign", ".afpub"), "Figma y XD": (".fig", ".xd", ".sketch"), "Cinema 4D": (".c4d",), "Blender": (".blend",), "Motion": (".motn",), "Plantillas": (".mogrt",), "Intercambio": (".fcpxml", ".aaf", ".edl", ".otio", ".xml"), } EXT_A_APP = {e: a for a, exts in APPS_PROYECTO.items() for e in exts} def app_proyecto(ext: str): """«.prproj» -> «Premiere». None si la extensión no es de un proyecto de edición o diseño.""" return EXT_A_APP.get((ext or "").lower()) BASURA_NOMBRES = {".ds_store", "thumbs.db", "desktop.ini", ".localized", ".bridgesort", ".bridgelabelsandratings"} # solo temporales y cachés que un programa regenera solo. NO se ponen aquí extensiones de datos # (.dat, .idx, .bak): son de plugins, subtítulos o copias que la gente guarda a propósito. BASURA_EXT = {".tmp", ".crdownload", ".part", ".pek", ".cfa", ".sfk"} 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"] # «cache» a secas no entra: es un nombre demasiado genérico para mandar la carpeta entera a _Para borrar PROXY_DIRS = {"proxy", "proxies", "proxy media", "proxytest", "_proxytest", "optimized media", "cache files", "render 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"(? «C0001».""" base = os.path.splitext(nombre)[0] b = base.lower() for suf in SUFIJOS_DERIVADO: if b.endswith(suf) and len(base) > len(suf): return base[:-len(suf)] return None def fecha_real(st): """El instante más antiguo que el disco sabe de este archivo. Al copiar en Windows, la fecha de creación pasa a ser la de la copia y la de modificación se conserva; al renderizar o reexportar, las dos son nuevas. Quedarse con la más antigua de las dos da siempre la mejor estimación de cuándo se grabó, sin tener que elegir una de antemano.""" c = getattr(st, "st_birthtime", None) or st.st_ctime return min(st.st_mtime, c) if c and c > 0 else st.st_mtime 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, separados por programa (Premiere, After Effects, Resolve, 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": "Le hago piezas mes a mes · se ordena por año y mes", "pauta": "Le hago anuncios y campañas · se ordena por campaña", "proyecto": "Un trabajo puntual con su entrega · se ordena por fases", "interno": "Es material de Labstream · se ordena por tipo de recurso", } # Destinos relativos a la raíz del cliente. Variables: {anio} {mes} ("08- Agosto") {nombre} {sub} # {fuente} (la cámara) {app} (el programa del proyecto) {rel} (la ruta tal cual venía). # Una variable vacía se lleva consigo su segmento: «01_Proyectos/{app}/{nombre}» sin programa queda # en «01_Proyectos/Reel botox». PLANTILLAS = { "contenido": { "MARCA": "00_Marca/{sub}", "PROYECTOS": "01_Proyectos/{app}/{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/{app}/{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/{app}/{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/{app}/{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", ".organilab")} # Rol de archivo suelto -> cubeta (para archivos que están fuera de sitio) # Un color por tipo de material. Se usa en el mapa, en el gráfico de peso y —si se quiere— en el # propio Explorador de Windows, poniéndole a cada carpeta un icono de ese color. COLOR_CUBETA = { "ANIO": "#4f8cff", "GRABACION": "#e0574f", "PIEZA": "#7c5cff", "CAMPANA": "#ff8c42", "EXPORTADOS": "#3ecf8e", "PORTADAS": "#f5b84b", "APOYOS": "#d96fd9", "MARCA": "#39b8c9", "PROYECTOS": "#8fa1b8", "MUSICA": "#c9b339", "SFX": "#c9b339", "VOZ": "#c9b339", "IA": "#a78bfa", "STOCK": "#5aa8e0", "GUION": "#9aa3ad", "FOTOS": "#f5b84b", "DOCUMENTOS": "#9aa3ad", "CELULAR": "#e0574f", "CONTENEDOR": "#6b7280", "REVISAR": "#ff6b6b", "PROXY": "#555c6b", "BASURA": "#3a3f4a", "APARTADO": "#9aa3ad", "": "#4b5563", } # Todo lo de la app en la carpeta del cliente vive aquí: los iconos de color, y lo que venga. CARPETA_APP = ".organilab" # Qué es cada rol dicho en llano, para que el mapa pueda explicar qué cae en cada carpeta. ROL_QUE = { "camara_raw": "originales RAW de cámara (.braw, .r3d, .ari)", "camara_video": "clips de cámara (URSA, Sony, DJI, Osmo, Atomos)", "camara_sidecar": "los .xml que Sony deja junto a cada clip", "audio_grabado": "audio de grabadora o micro (DJI Mic, Tascam)", "celular_video": "video de celular y de WhatsApp", "celular_foto": "fotos de celular y de WhatsApp", "foto_camara": "fotos de cámara (.arw, .cr3, .nef, .dng)", "ia": "material generado con IA", "stock": "tomas de stock (Envato y similares)", "captura": "capturas de pantalla", "proxy": "proxies (.lrf y los que crean Resolve y Premiere)", "video": "video que no viene de cámara: exportados y masters", "audio": "música y audio (.wav, .mp3, .aac)", "imagen": "imágenes (.jpg, .png, .tif)", "diseno": "diseño (.psd, .ai, .indd, Affinity)", "proyecto": "proyectos de edición (.prproj, .aep, .drp)", "documento": "documentos, guiones y subtítulos (.pdf, .docx, .srt)", "fuente": "tipografías (.ttf, .otf)", "comprimido": "comprimidos (.zip, .rar)", "lut": "LUTs (.cube, .3dl)", "programa": "instaladores y ejecutables", "otro": "lo que no encaja en ninguna otra cosa", "basura": "basura de sistema y cachés", } 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", }