- Cada carpeta lleva el color de lo que guarda (R.COLOR_CUBETA). Lo que
cuelga de una carpeta con color hereda el suyo; una que mezcla tipos se
queda neutra.
- «¿Dónde va a parar este archivo?»: donde_iria() resuelve cualquier nombre
con el mismo clasificador del escaneo y el árbol enciende el destino.
Debajo, nombres de archivo de verdad de la carpeta conectada.
- Renombrar una carpeta del árbol cambia TODAS las plantillas que pasan por
ella (00_Grabacion arrastra a CELULAR) y, si se pide, las carpetas que ya
existen en el disco; queda en la bitácora y se deshace.
- {anio} y {mes} dejan de encajar con cualquier carpeta: solo con algo que
parezca un año o un mes.
- Colores en el Explorador: .ico generados sin dependencias y asignados con
SHGetSetFolderCustomSettings, la misma API de «Cambiar icono». Escribir el
desktop.ini a mano NO basta, y SHGetFileInfo no sirve para comprobarlo:
no informa de la personalización ni cuando la puso el propio Explorador.
Ruta de icono relativa, así que los colores viajan con el disco. Si el
disco no deja marcar la carpeta, se dice y no se deja nada escrito.
- .organilab no se escanea y los desktop.ini de la app no son basura.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
670 líneas
32 KiB
Python
670 líneas
32 KiB
Python
# -*- 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"(?<!\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)
|
|
|
|
|
|
# sufijos que la propia app (o el editor) le pone a un archivo derivado de otro: el derivado se
|
|
# genera mucho después, pero la fecha que importa es la del material original
|
|
SUFIJOS_DERIVADO = ("_10bit", "_ligero", "_h264", "_proxy", "_lt", "_hq", "_prores", "_convertido",
|
|
"_export", "_render", "_master", "_final")
|
|
|
|
|
|
def base_derivado(nombre: str):
|
|
"""Nombre del archivo del que este parece salir, o None. «C0001_10bit.mp4» -> «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",
|
|
}
|