Skip to content

alc.file

The filesystem domain — models that describe how files behave. AlcFile.load() is polymorphic: it returns the correct concrete subclass by extension.

Core

alc.file.core

AlcFile

AlcFile(path: Union[str, Path, dict], **kwargs)

Bases: BaseModel

Source code in alc\file\core.py
def __init__(self, path: Union[str, Path, dict], **kwargs):
    # 1. Handle the positional argument
    if isinstance(path, (str, Path)):
        kwargs["path"] = path
    elif isinstance(path, dict):
        kwargs.update(path)

    # 2. Pass everything to the Pydantic init as keyword arguments
    super().__init__(**kwargs)

file_type property

file_type: FileType

Determines the file type based on extension mapping.

git_status property

git_status: str

Calculates the git status for this specific file. Returns: 'untracked', 'modified', 'staged', or 'clean'.

is_valid property

is_valid: bool

Checks if the CURRENT filename on disk matches pipeline standards.

is_prod_file property

is_prod_file: bool

This is a valid production file path. Returns:

resolve_class classmethod

resolve_class(path: Union[str, Path]) -> Type['AlcFile']

Inspects a path string or object, identifies its FileType, and returns the exact concrete subclass mapped to handle it across our pipeline tools.

Source code in alc\file\core.py
@classmethod
def resolve_class(cls, path: Union[str, Path]) -> Type["AlcFile"]:
    """
    Inspects a path string or object, identifies its FileType, and returns
    the exact concrete subclass mapped to handle it across our pipeline tools.
    """
    from alc.file.images import AlcImage  # Local import to prevent circularity
    from alc.file.video import AlcMovie

    clean_path = Path(path)
    ft = get_file_type(clean_path)

    # Maps file type extensions to their corresponding specialized class modules
    mapping = {
        FileType.IMAGE: AlcImage,
        FileType.PSD: AlcImage,
        FileType.PDF: AlcPDF,
        FileType.MOVIE: AlcMovie,
        # If you add FileType.AUDIO or FileType.MESH later, map them straight in here!
    }

    target_loader = mapping.get(ft)
    if not target_loader:
        return cls  # Fallback to generic AlcFile if no custom module exists

    # If it returns AlcImage, we let AlcImage inspect deeper (e.g., .exr vs .png vs .psd)
    if hasattr(target_loader, "load"):
        # If resolve_class is called by AlcImage itself, prevent infinite loops
        if cls == target_loader:
            return target_loader

        # Ask the image factory sub-switchboard what concrete class it maps to
        # (We will add a clean class helper to AlcImage in Step 2)
        if hasattr(target_loader, "resolve_image_class"):
            return target_loader.resolve_image_class(clean_path)

    return target_loader

auto_sanitize_path classmethod

auto_sanitize_path(data: Any) -> Any

Intercepts the input data. If it's a dict with a 'path' or a raw Path/str, it ensures the filename portion is sanitized and RENAMED before the model is created.

Source code in alc\file\core.py
@model_validator(mode="before")
@classmethod
def auto_sanitize_path(cls, data: Any) -> Any:
    """
    Intercepts the input data. If it's a dict with a 'path' or a raw Path/str,
    it ensures the filename portion is sanitized and RENAMED before the model is created.
    """
    # Handle if data is just the path string/object or a dict
    path_val = data.get("path") if isinstance(data, dict) else data
    path_val = Path(path_val)

    if path_val:
        p = Path(path_val)
        clean_name = get_valid_filename(p.name)
        clean_path = p.with_name(clean_name)
        if path_val.exists() and path_val != clean_path:
            try:
                path_val.rename(clean_path)
                move_thumb_sidecar(path_val, clean_path)
            except OSError as e:
                print(f"Warning: Could not rename file: {e}")
                clean_path = path_val

        if isinstance(data, dict):
            data["path"] = clean_path
        else:
            data = {"path": clean_path}

    return data

new

new(overwrite: bool = False, **kwargs) -> 'AlcFile'

Lifecycle hook called on an instance. Resolves the correct concrete subclass, initializes the asset structure on disk, and returns the final mutated object layer.

Source code in alc\file\core.py
def new(self, overwrite: bool = False, **kwargs) -> "AlcFile":
    """
    Lifecycle hook called on an instance. Resolves the correct concrete subclass,
    initializes the asset structure on disk, and returns the final mutated object layer.
    """
    # 1. Look up the true structural class based on this instance's path
    concrete_class = self.resolve_class(self.path)

    # 2. If it's a subclass (like BaseImage or PNGImage), delegate to its handler
    if concrete_class != self.__class__:
        # We instantiate the subclass model first, then call its specific .new() implementation
        sub_instance = concrete_class(path=self.path, tags=self.tags)
        return sub_instance.new(overwrite=overwrite, **kwargs)

    # 3. Fallback: Generic empty 0-byte touch logic for baseline files
    if self.path.exists() and not overwrite:
        logger.info(
            f"✨ Lifecycle Skip: Resource track already allocated at {self.path}"
        )
        return self

    self.path.parent.mkdir(parents=True, exist_ok=True)

    try:
        if not self.path.exists() or overwrite:
            self.path.touch(exist_ok=True)
            logger.info(
                f"📁 Resource Initialized: Empty baseline file allocated at {self.path}"
            )
        return self
    except Exception as err:
        logger.error(f"❌ Error creating target file node context: {err}")
        raise

load

load() -> Any

Reads the current instance track, matches its true polymorphic subclass type, and safely transforms it into a fully active feature layout object.

Source code in alc\file\core.py
def load(self) -> Any:
    """
    Reads the current instance track, matches its true polymorphic subclass type,
    and safely transforms it into a fully active feature layout object.
    """
    concrete_class = self.resolve_class(self.path)

    if concrete_class == self.__class__:
        return self  # Already perfectly instantiated

    # Otherwise, polymorphically instantiate the class over the active path data keys
    return concrete_class(path=self.path, tags=self.tags)

is_cookbook_file

is_cookbook_file() -> bool

Checks if the file is located within the cookbook directory. Returns:

Source code in alc\file\core.py
@computed_field
def is_cookbook_file(self) -> bool:
    """
    Checks if the file is located within the cookbook directory.
    Returns:

    """
    from alc.config import CFG

    cookbook_dir = Path(CFG.cookbook_dir).as_posix()
    return cookbook_dir in self.path.as_posix()

ensure_valid_filename

ensure_valid_filename() -> 'AlcFile'

Rename this file on disk to a pipeline-valid name if it isn't one already.

No-op when the current filename already passes is_valid_filename. Otherwise sanitizes the name via :func:get_valid_filename, renames the file in place (de-duping with a _N suffix if the clean name is already taken by a different file), and updates self.path. Returns self so callers can chain.

This is the explicit counterpart to the auto_sanitize_path validator (which fixes names implicitly on construction): a safe, collision-aware entry point the UI/right-click menu can call on demand.

Source code in alc\file\core.py
def ensure_valid_filename(self) -> "AlcFile":
    """Rename this file on disk to a pipeline-valid name if it isn't one already.

    No-op when the current filename already passes ``is_valid_filename``.
    Otherwise sanitizes the name via :func:`get_valid_filename`, renames the
    file in place (de-duping with a ``_N`` suffix if the clean name is already
    taken by a *different* file), and updates ``self.path``. Returns ``self``
    so callers can chain.

    This is the explicit counterpart to the ``auto_sanitize_path`` validator
    (which fixes names implicitly on construction): a safe, collision-aware
    entry point the UI/right-click menu can call on demand.
    """
    if self.is_valid:
        return self

    target = self.path.with_name(get_valid_filename(self.path.name))

    # Don't clobber a *different* file already sitting at the clean name.
    if target != self.path and target.exists():
        stem, suffix = target.stem, target.suffix
        i = 1
        while target.exists():
            target = target.with_name(f"{stem}_{i}{suffix}")
            i += 1

    if self.path.exists() and target != self.path:
        self.path.rename(target)
        move_thumb_sidecar(self.path, target)
        logger.info(f"Renamed to valid filename: {self.path.name} -> {target.name}")

    self.path = target
    return self

rename

rename(new_name: str, sanitize: bool = True) -> 'AlcFile'

Rename this file on disk to new_name (a bare filename, same folder).

The pipeline-valid-name rule is enforced here rather than at the callsite: new_name is run through :func:get_valid_filename unless sanitize is False. An omitted extension keeps the current one, so the UI can offer a stem-only edit without artists dropping the .png. The beside-file thumb sidecar (.thumb/{stem}.thumb.png) travels with the file so the row keeps its picture. Refuses to clobber an existing file. Returns self.

Source code in alc\file\core.py
def rename(self, new_name: str, sanitize: bool = True) -> "AlcFile":
    """Rename this file on disk to *new_name* (a bare filename, same folder).

    The pipeline-valid-name rule is enforced here rather than at the callsite:
    *new_name* is run through :func:`get_valid_filename` unless ``sanitize`` is
    False. An omitted extension keeps the current one, so the UI can offer a
    stem-only edit without artists dropping the ``.png``. The beside-file thumb
    sidecar (``.thumb/{stem}.thumb.png``) travels with the file so the row keeps
    its picture. Refuses to clobber an existing file. Returns ``self``.
    """
    name = (new_name or "").strip()
    if not name:
        raise ValueError("No new name provided")
    if "/" in name or "\\" in name:
        raise ValueError("New name must be a filename, not a path")

    # Keep the current extension when the caller edits the stem only.
    if not Path(name).suffix:
        name = f"{name}{self.path.suffix}"
    if sanitize:
        name = get_valid_filename(name)
    if not name:
        raise ValueError("New name is empty after sanitizing")

    target = self.path.with_name(name)
    if target == self.path:
        return self
    if target.exists():
        raise FileExistsError(f"{name} already exists in this folder")
    if not self.path.exists():
        raise FileNotFoundError(f"{self.path.name} no longer exists on disk")

    self.path.rename(target)
    move_thumb_sidecar(self.path, target)
    logger.info(f"Renamed: {self.path.name} -> {target.name}")
    self.path = target
    return self

delete

delete(missing_ok: bool = True) -> bool

Delete this file from disk, along with its beside-file thumb sidecar.

Returns True if the file was there and is now gone. With missing_ok an already-absent file is a no-op returning False, not an error.

Source code in alc\file\core.py
def delete(self, missing_ok: bool = True) -> bool:
    """Delete this file from disk, along with its beside-file thumb sidecar.

    Returns True if the file was there and is now gone. With ``missing_ok``
    an already-absent file is a no-op returning False, not an error.
    """
    if not self.path.exists():
        if missing_ok:
            return False
        raise FileNotFoundError(f"{self.path.name} no longer exists on disk")
    if self.path.is_dir():
        raise IsADirectoryError(f"{self.path.name} is a folder, not a file")

    thumb = self.file_thumb_path
    self.path.unlink()
    logger.info(f"Deleted: {self.path}")
    if thumb.exists():
        try:
            thumb.unlink()
        except OSError:
            logger.warning("Failed to delete thumb sidecar", exc_info=True)
    return True

copy

copy(destination: str | Path)

Copies the file on disk to destination Returns:

Source code in alc\file\core.py
def copy(self, destination: str | Path):
    """
    Copies the file on disk to destination
    Returns:

    """

    pass

ensure_file_thumb

ensure_file_thumb(force=False)

If it's an image or video, make sure a FRESH .thumb exists.

Regenerates a STALE thumb — one whose source was written more recently than the thumb — so re-rendered media (e.g. a style frame re-rendered to the same path) refreshes instead of serving the old picture. force always rebuilds.

Source code in alc\file\core.py
def ensure_file_thumb(self, force=False):
    """If it's an image or video, make sure a FRESH .thumb exists.

    Regenerates a STALE thumb — one whose source was written more recently than
    the thumb — so re-rendered media (e.g. a style frame re-rendered to the same
    path) refreshes instead of serving the old picture. ``force`` always rebuilds.
    """
    f_thumb = self.file_thumb_path
    if not force and f_thumb.exists():
        try:
            fresh = f_thumb.stat().st_mtime >= self.path.stat().st_mtime
        except OSError:
            fresh = True  # can't compare → don't churn; treat as good
        if fresh:
            return True

    asset = self.load()
    if hasattr(asset, "create_file_thumb"):
        thumb_path = asset.create_file_thumb()
        thumb_path = Path(thumb_path)
        if thumb_path.exists():
            return True
        else:
            logger.error(f"Failed to save {thumb_path}")
            return False

BaseCode

BaseCode(path: Union[str, Path, dict], **kwargs)

Bases: AlcFile

Base model for source code files.

Source code in alc\file\core.py
def __init__(self, path: Union[str, Path, dict], **kwargs):
    # 1. Handle the positional argument
    if isinstance(path, (str, Path)):
        kwargs["path"] = path
    elif isinstance(path, dict):
        kwargs.update(path)

    # 2. Pass everything to the Pydantic init as keyword arguments
    super().__init__(**kwargs)

PythonFile

PythonFile(path: Union[str, Path, dict], **kwargs)

Bases: BaseCode

Specific model for Python scripts with execution/linting logic.

Source code in alc\file\core.py
def __init__(self, path: Union[str, Path, dict], **kwargs):
    # 1. Handle the positional argument
    if isinstance(path, (str, Path)):
        kwargs["path"] = path
    elif isinstance(path, dict):
        kwargs.update(path)

    # 2. Pass everything to the Pydantic init as keyword arguments
    super().__init__(**kwargs)

format_code

format_code()

Runs black formatting on the file.

Source code in alc\file\core.py
def format_code(self):
    """Runs black formatting on the file."""
    try:
        import black

        content = self.read()
        formatted = black.format_str(content, mode=black.Mode())
        if content != formatted:
            self.write(formatted)
            return True
    except Exception as e:
        logger.error(f"Black formatting failed: {e}")
    return False

check_syntax

check_syntax() -> Optional[str]

Verify the script actually compiles.

Source code in alc\file\core.py
def check_syntax(self) -> Optional[str]:
    """Verify the script actually compiles."""
    try:
        compile(self.read(), self.path.name, "exec")
        return None
    except SyntaxError as e:
        return f"Line {e.lineno}: {e.msg}"

ExtractedImage

Bases: BaseModel

One image pulled out of a PDF, held in memory before it's written.

xref is PyMuPDF's cross-reference id for the embedded stream — it is unique per stored image, so it's the key we dedupe on (a logo reused on every slide has one xref, not N).

AlcPDF

AlcPDF(path: Union[str, Path, dict], **kwargs)

Bases: AlcFile

A PDF document. The pipeline home for anything we do with PDFs — starting with pulling embedded raster images back out.

We already convert screenplay PDFs to .fdx over in alc.models.script.models.pdf; that stays where it is (it's a text/geometry problem). This class is the file-object handle for the binary/asset side: images now, page rasterization / text / split-merge later.

Backed by PyMuPDF (fitz), the same dependency the script parser uses.

Source code in alc\file\core.py
def __init__(self, path: Union[str, Path, dict], **kwargs):
    # 1. Handle the positional argument
    if isinstance(path, (str, Path)):
        kwargs["path"] = path
    elif isinstance(path, dict):
        kwargs.update(path)

    # 2. Pass everything to the Pydantic init as keyword arguments
    super().__init__(**kwargs)

images_dir property

images_dir: Path

Default output folder: <file.parent>/<stem>_images/.

create_thumb

create_thumb(
    width: int = 256,
    height: int = 256,
    thumb_type: str = "file",
    thumb_path: Path = None,
    page: int = 0,
) -> Path

Rasterize page (0-based) and write a fitted PNG thumbnail.

Resolves the destination the same way :meth:BaseImage.create_thumb does — the beside-file file_thumb_path for thumb_type="file", otherwise the PathObject .thumb convention for the given type — unless an explicit thumb_path is passed. Aspect is preserved (documents are portrait; a hard square-pad would letterbox them).

Source code in alc\file\core.py
def create_thumb(
    self,
    width: int = 256,
    height: int = 256,
    thumb_type: str = "file",  # file | version | task | asset
    thumb_path: Path = None,
    page: int = 0,
) -> Path:
    """Rasterize *page* (0-based) and write a fitted PNG thumbnail.

    Resolves the destination the same way :meth:`BaseImage.create_thumb`
    does — the beside-file ``file_thumb_path`` for ``thumb_type="file"``,
    otherwise the PathObject ``.thumb`` convention for the given type — unless
    an explicit *thumb_path* is passed. Aspect is preserved (documents are
    portrait; a hard square-pad would letterbox them).
    """
    from PIL import Image

    if thumb_path is None:
        if thumb_type == "file":
            thumb_path = self.file_thumb_path
        else:
            thumb_path = self.path_object.get_thumb_path(thumb_type=thumb_type)
    thumb_path = Path(thumb_path)

    img = render_pdf_page(self.path, page=page)
    img.thumbnail((width, height), Image.Resampling.LANCZOS)
    thumb_path.parent.mkdir(parents=True, exist_ok=True)
    img.save(thumb_path, format="PNG")
    logger.info(f"Alchemy PDF: Thumbnail created. {thumb_path}")
    return thumb_path

extract_images

extract_images(
    min_pixels: int = 0, dedupe: bool = True
) -> List[ExtractedImage]

Pull every embedded raster image out of the PDF, in memory. Thin wrapper over :func:extract_pdf_images (the free function callers can use without constructing an AlcFile).

Source code in alc\file\core.py
def extract_images(
    self, min_pixels: int = 0, dedupe: bool = True
) -> List[ExtractedImage]:
    """Pull every embedded raster image out of the PDF, in memory. Thin wrapper
    over :func:`extract_pdf_images` (the free function callers can use without
    constructing an AlcFile)."""
    return extract_pdf_images(self.path, min_pixels=min_pixels, dedupe=dedupe)

export_images

export_images(
    out_dir: Optional[Union[str, Path]] = None,
    min_pixels: int = 0,
    dedupe: bool = True,
    overwrite: bool = True,
) -> List[Path]

Extract images and write them to disk (default <stem>_images/). Thin wrapper over :func:export_pdf_images.

Source code in alc\file\core.py
def export_images(
    self,
    out_dir: Optional[Union[str, Path]] = None,
    min_pixels: int = 0,
    dedupe: bool = True,
    overwrite: bool = True,
) -> List[Path]:
    """Extract images and write them to disk (default ``<stem>_images/``).
    Thin wrapper over :func:`export_pdf_images`."""
    return export_pdf_images(
        self.path,
        out_dir or self.images_dir,
        min_pixels=min_pixels,
        dedupe=dedupe,
        overwrite=overwrite,
    )

get_valid_filename

get_valid_filename(filename: str) -> str

Standardizes filenames for pipeline safety: - Removes accents and converts to ASCII. - Replaces spaces, hyphens, and multi-dots with single underscores. - Removes non-alphanumeric characters. - Forces lowercase for cross-platform consistency.

Source code in alc\file\core.py
def get_valid_filename(filename: str) -> str:
    """
    Standardizes filenames for pipeline safety:
    - Removes accents and converts to ASCII.
    - Replaces spaces, hyphens, and multi-dots with single underscores.
    - Removes non-alphanumeric characters.
    - Forces lowercase for cross-platform consistency.
    """
    if not filename:
        return ""

    # 1. Separate stem and extension to avoid mangling the extension dot
    file_path = Path(filename)
    stem = file_path.stem
    suffix = file_path.suffix.lower()

    # 1b. Preserve a trailing frame-number block (e.g. 'shot.0001' -> keep the
    # '.0001'). Image-sequence frames use a dot delimiter before the frame, and
    # stripping it (-> 'shot0001') silently renames/breaks sequences on disk.
    frame_part = ""
    frame_match = re.search(r"\.(\d+)$", stem)
    if frame_match:
        frame_part = f".{frame_match.group(1)}"
        stem = stem[: frame_match.start()]

    # 2. Normalize unicode (removes accents like 'é' -> 'e')
    stem = unicodedata.normalize("NFKD", stem).encode("ascii", "ignore").decode("ascii")

    # 3. Replace spaces, hyphens, and brackets with underscores
    stem = re.sub(r"[\s\-\[\]\(\)\{\}]+", "_", stem)

    # 4. Remove any remaining "illegal" characters (only allow letters, numbers, underscores)
    stem = re.sub(r"[^a-zA-Z0-9_]", "", stem)

    # 5. Clean up multiple underscores: 'test__file' -> 'test_file'
    stem = re.sub(r"_+", "_", stem).strip("_")

    # 6. Force lowercase for cross-platform consistency, so the result always
    # satisfies is_valid_filename() (the pipeline's lowercase-only standard).
    stem = stem.lower()

    # 7. Re-attach the preserved frame block, then the extension.
    return f"{stem}{frame_part}{suffix}"

is_valid_filename

is_valid_filename(filename: str) -> bool

Checks if a filename adheres to pipeline standards: 1. No spaces or hyphens (underscores only). 2. No special characters (only a-z, 0-9, _, .). 3. No leading/trailing underscores or dots. 4. Lowercase only (standard for many VFX/Animation pipelines).

Source code in alc\file\core.py
def is_valid_filename(filename: str) -> bool:
    """
    Checks if a filename adheres to pipeline standards:
    1. No spaces or hyphens (underscores only).
    2. No special characters (only a-z, 0-9, _, .).
    3. No leading/trailing underscores or dots.
    4. Lowercase only (standard for many VFX/Animation pipelines).
    """
    if not filename:
        return False

    # Define the strict pattern:
    # Starts with alphanumeric, contains alphanumeric/underscores/dots, ends with alphanumeric
    # This prevents filenames like ".config" or "file_."
    pattern = r"^[a-z0-9][a-z0-9._]*[a-z0-9]$"

    # Check if the filename matches the strict pattern
    if re.match(pattern, filename):
        return True

    return False

move_thumb_sidecar

move_thumb_sidecar(old_path: Path, new_path: Path) -> None

Carry a file's beside-file thumb (.thumb/{stem}.thumb.png) across a rename.

Every path that renames a file on disk goes through here, so a renamed row keeps its picture instead of falling back to the generic file icon. Best-effort — a missing or locked sidecar never fails the rename itself.

Source code in alc\file\core.py
def move_thumb_sidecar(old_path: Path, new_path: Path) -> None:
    """Carry a file's beside-file thumb (``.thumb/{stem}.thumb.png``) across a rename.

    Every path that renames a file on disk goes through here, so a renamed row keeps
    its picture instead of falling back to the generic file icon. Best-effort — a
    missing or locked sidecar never fails the rename itself.
    """
    from alc.path import file_thumb_path as ftp

    old_thumb, new_thumb = ftp(old_path), ftp(new_path)
    if old_thumb == new_thumb or not old_thumb.exists():
        return
    try:
        new_thumb.parent.mkdir(parents=True, exist_ok=True)
        if new_thumb.exists():
            new_thumb.unlink()
        old_thumb.rename(new_thumb)
    except OSError:
        logger.warning("Failed to move thumb sidecar", exc_info=True)

render_pdf_page

render_pdf_page(
    path: Union[str, Path], page: int = 0, zoom: float = 2.0
)

Rasterize one page of a PDF to a flattened RGB PIL image (white bg).

A free function (not a method) so callers can render a page WITHOUT constructing an :class:AlcFile — which would sanitize/rename the file on disk. That matters for passive/read-only contexts like the ingest browser, where the source must stay untouched. page is 0-based (clamped into range); zoom is the render scale (2.0 ≈ 144 dpi) for a crisp thumbnail.

Source code in alc\file\core.py
def render_pdf_page(path: Union[str, Path], page: int = 0, zoom: float = 2.0):
    """Rasterize one page of a PDF to a flattened RGB PIL image (white bg).

    A free function (not a method) so callers can render a page WITHOUT
    constructing an :class:`AlcFile` — which would sanitize/rename the file on
    disk. That matters for passive/read-only contexts like the ingest browser,
    where the source must stay untouched. ``page`` is 0-based (clamped into
    range); ``zoom`` is the render scale (2.0 ≈ 144 dpi) for a crisp thumbnail.
    """
    import fitz  # PyMuPDF — lazy so importing this module stays cheap
    from PIL import Image

    with fitz.open(path) as doc:
        if doc.page_count == 0:
            raise ValueError(f"PDF has no pages: {path}")
        page_no = max(0, min(page, doc.page_count - 1))
        pix = doc.load_page(page_no).get_pixmap(
            matrix=fitz.Matrix(zoom, zoom), alpha=False
        )
        return Image.frombytes("RGB", (pix.width, pix.height), pix.samples)

extract_pdf_images

extract_pdf_images(
    pdf_path: Union[str, Path],
    min_pixels: int = 0,
    dedupe: bool = True,
) -> List["ExtractedImage"]

Pull every embedded raster image out of a PDF, in memory.

A free function (not a method) so it runs WITHOUT constructing an :class:AlcFile — which would sanitize/rename the file on disk. Ingest reads the dropped PDF read-only, so extraction must not touch the source. Returns :class:ExtractedImage in encounter order; bytes stay in the native stored encoding (a JPEG stays a JPEG). min_pixels drops slivers (w*h); dedupe collapses repeated xref streams (decks reuse logos/backplates heavily).

Source code in alc\file\core.py
def extract_pdf_images(
    pdf_path: Union[str, Path], min_pixels: int = 0, dedupe: bool = True
) -> List["ExtractedImage"]:
    """Pull every embedded raster image out of a PDF, in memory.

    A free function (not a method) so it runs WITHOUT constructing an
    :class:`AlcFile` — which would sanitize/rename the file on disk. Ingest reads
    the dropped PDF read-only, so extraction must not touch the source. Returns
    :class:`ExtractedImage` in encounter order; bytes stay in the native stored
    encoding (a JPEG stays a JPEG). ``min_pixels`` drops slivers (w*h); ``dedupe``
    collapses repeated ``xref`` streams (decks reuse logos/backplates heavily)."""
    import fitz  # PyMuPDF — lazy so importing this module stays cheap

    results: List[ExtractedImage] = []
    seen: set[int] = set()
    with fitz.open(pdf_path) as doc:
        for page_no in range(doc.page_count):
            page = doc.load_page(page_no)
            for img in page.get_images(full=True):
                xref = img[0]
                if dedupe and xref in seen:
                    continue
                seen.add(xref)
                try:
                    info = doc.extract_image(xref)
                except Exception as e:  # corrupt / unsupported stream
                    logger.warning(f"Could not extract image xref={xref}: {e}")
                    continue
                if not info or not info.get("image"):
                    continue
                w, h = info.get("width", 0), info.get("height", 0)
                if min_pixels and (w * h) < min_pixels:
                    continue
                results.append(
                    ExtractedImage(
                        xref=xref,
                        page=page_no,
                        index=len(results),
                        ext=(info.get("ext") or "png").lower(),
                        data=info["image"],
                        width=w,
                        height=h,
                    )
                )
    return results

export_pdf_images

export_pdf_images(
    pdf_path: Union[str, Path],
    out_dir: Optional[Union[str, Path]] = None,
    min_pixels: int = 0,
    dedupe: bool = True,
    overwrite: bool = True,
) -> List[Path]

Extract a PDF's embedded images and write them to disk (non-destructive — see :func:extract_pdf_images). Files land in out_dir (default <parent>/<stem>_images/) named <stem>_p<page>_<index>.<ext> — 1-based page, zero-padded index — so they sort in document order and never collide. Returns the written paths.

Source code in alc\file\core.py
def export_pdf_images(
    pdf_path: Union[str, Path],
    out_dir: Optional[Union[str, Path]] = None,
    min_pixels: int = 0,
    dedupe: bool = True,
    overwrite: bool = True,
) -> List[Path]:
    """Extract a PDF's embedded images and write them to disk (non-destructive —
    see :func:`extract_pdf_images`). Files land in *out_dir* (default
    ``<parent>/<stem>_images/``) named ``<stem>_p<page>_<index>.<ext>`` — 1-based
    page, zero-padded index — so they sort in document order and never collide.
    Returns the written paths."""
    pdf_path = Path(pdf_path)
    target = Path(out_dir) if out_dir else pdf_path.parent / f"{pdf_path.stem}_images"
    images = extract_pdf_images(pdf_path, min_pixels=min_pixels, dedupe=dedupe)
    if not images:
        logger.info(f"No embedded images found in {pdf_path.name}")
        return []

    target.mkdir(parents=True, exist_ok=True)
    stem = pdf_path.stem
    written: List[Path] = []
    for im in images:
        name = f"{stem}_p{im.page + 1:03d}_{im.index:03d}.{im.ext}"
        dest = target / name
        if dest.exists() and not overwrite:
            written.append(dest)
            continue
        dest.write_bytes(im.data)
        written.append(dest)
    logger.info(f"Exported {len(written)} image(s) from {pdf_path.name} -> {target}")
    return written

Images

alc.file.images

BaseImage

BaseImage(path: Union[str, Path, dict], **kwargs)

Bases: AlcFile

Source code in alc\file\images.py
def __init__(self, path: Union[str, Path, dict], **kwargs):
    super().__init__(path, **kwargs)

resolution property

resolution: Tuple[int, int]

Returns (width, height) for the image.

info property

info: Tuple[int, int]

Lazy load resolution only when requested.

render_path property

render_path

returns the render_dir equivalent of the current source file. Returns:

to_img

to_img() -> Image.Image

Default implementation: Loads the image using standard PIL. Subclasses (like EXRImage or PSDImageFile) override this.

Source code in alc\file\images.py
def to_img(self) -> Image.Image:
    """
    Default implementation: Loads the image using standard PIL.
    Subclasses (like EXRImage or PSDImageFile) override this.
    """
    if self.img is not None:
        return self.img
    try:
        self.img = Image.open(self.path)
        if self.img.mode not in ("RGB", "RGBA"):
            self.img = self.img.convert("RGBA")
    except Exception as e:
        logger.error(f"Failed to load image at {self.path}: {e}")
        raise
    return self.img

crop

crop(x: int, y: int, w: int, h: int) -> Image.Image

Crops the image based on (x, y, width, height) coordinates. Standard for web-based cropping tools.

Source code in alc\file\images.py
def crop(self, x: int, y: int, w: int, h: int) -> Image.Image:
    """
    Crops the image based on (x, y, width, height) coordinates.
    Standard for web-based cropping tools.
    """
    img = self.to_img()

    # PIL expects (left, top, right, bottom)
    left = x
    top = y
    right = x + w
    bottom = y + h

    # Perform the crop
    self.img = img.crop((left, top, right, bottom))

    # We log this because it changes the resolution/aspect ratio fundamentally
    logging.info(f"Alchemy Image: Cropped to {self.img.size}")

    return self.img

crop_normalized

crop_normalized(
    x: float, y: float, w: float, h: float
) -> Image.Image

Crops using normalized coordinates (0.0 - 1.0). Essential for resolution-independent cropping.

Source code in alc\file\images.py
def crop_normalized(self, x: float, y: float, w: float, h: float) -> Image.Image:
    """
    Crops using normalized coordinates (0.0 - 1.0).
    Essential for resolution-independent cropping.
    """
    full_w, full_h = self.resolution

    real_x = int(x * full_w)
    real_y = int(y * full_h)
    real_w = int(w * full_w)
    real_h = int(h * full_h)

    return self.crop(real_x, real_y, real_w, real_h)

create_proxy

create_proxy(
    proxy_path: Path,
    width: int = RES_CONFIG.proxy[0],
    height: int = RES_CONFIG.proxy[1],
    suffix: str = "_proxy",
) -> Path

Fits the image and saves it as a new file.

Source code in alc\file\images.py
def create_proxy(
    self,
    proxy_path: Path,
    width: int = RES_CONFIG.proxy[0],
    height: int = RES_CONFIG.proxy[1],
    suffix: str = "_proxy",
) -> Path:
    """Fits the image and saves it as a new file."""
    proxy_path = self.path.with_name(f"{self.path.stem}{suffix}.png")

    self.fit(width, height)

    # 2. Save the result
    self.save_png(proxy_path)

    # 3. Return a NEW Pydantic model for the proxy
    return proxy_path

new

new(
    width: int = 1280,
    height: int = 720,
    overwrite: bool = False,
    **kwargs
) -> "BaseImage"

Specialized instance override. Bakes an RGBA canvas to disk and pins the PIL object reference directly to self.img for immediate mutation.

Source code in alc\file\images.py
def new(
    self, width: int = 1280, height: int = 720, overwrite: bool = False, **kwargs
) -> "BaseImage":
    """
    Specialized instance override. Bakes an RGBA canvas to disk and pins
    the PIL object reference directly to self.img for immediate mutation.
    """
    if self.path.exists() and not overwrite:
        logger.info(
            f"✨ Canvas Skip: Active image file already present at {self.path}"
        )
        return self

    self.path.parent.mkdir(parents=True, exist_ok=True)

    try:
        # Create the 1080p transparent canvas image track
        blank_canvas = Image.new("RGBA", (width, height), (255, 255, 2555, 255))
        blank_canvas.save(self.path, format="PNG")

        # Pin the in-memory PIL object directly into this active instance!
        self.img = blank_canvas
        logger.info(
            f"🎨 Canvas Initialized: Fresh {width}x{height} blank frame written to {self.path}"
        )
        return self

    except Exception as err:
        logger.error(f"❌ Failed to instantiate image asset canvas trace: {err}")
        raise

PSDImageFile

PSDImageFile(path: Union[str, Path, dict], **kwargs)

Bases: BaseImage

Source code in alc\file\images.py
def __init__(self, path: Union[str, Path, dict], **kwargs):
    super().__init__(path, **kwargs)

save_psd

save_psd(
    output_path: Path = None,
    layers: list = None,
    metadata: dict = None,
    create_thumb: bool = True,
) -> str

Save as a layered PSD using psd-tools PixelLayer.frompil().

list of dicts, bottom-to-top order:

{ 'name': str, 'image': PIL.Image (RGBA), 'opacity': int 0-255 (default 255), 'visible': bool (default True), }

The instance's own image is always written as the bottom 'Background' layer. metadata dict is embedded as a JSON string in a tEXt-style tagged block.

Source code in alc\file\images.py
def save_psd(
    self,
    output_path: Path = None,
    layers: list = None,
    metadata: dict = None,
    create_thumb: bool = True,
) -> str:
    """
    Save as a layered PSD using psd-tools PixelLayer.frompil().

    layers: list of dicts, bottom-to-top order:
        {
            'name':    str,
            'image':   PIL.Image (RGBA),
            'opacity': int 0-255  (default 255),
            'visible': bool       (default True),
        }
    The instance's own image is always written as the bottom 'Background' layer.
    metadata dict is embedded as a JSON string in a tEXt-style tagged block.
    """
    from psd_tools.api.layers import PixelLayer

    output_path = Path(output_path or self.path.with_suffix(".psd"))
    output_path.parent.mkdir(parents=True, exist_ok=True)

    base = self.to_img().convert("RGBA")
    w, h = base.size
    layers = layers or []

    psd = PSDImage.new("RGBA", (w, h))

    # Background is always the bottom layer
    bg = PixelLayer.frompil(base, psd, name="Background")
    psd.append(bg)

    # Additional layers, bottom-to-top
    for ld in layers:
        img = ld.get("image")
        if img is None:
            continue
        img_rgba = img.convert("RGBA")
        if img_rgba.size != (w, h):
            img_rgba = img_rgba.resize((w, h), Image.Resampling.LANCZOS)
        layer = PixelLayer.frompil(img_rgba, psd, name=ld.get("name", "Layer"))
        layer.opacity = ld.get("opacity", 255)
        layer.visible = ld.get("visible", True)
        psd.append(layer)

    if metadata:
        # Store full metadata JSON in a tagged block on the PSD itself
        # so Alchemy can roundtrip settings when re-opening the file
        try:
            from psd_tools.constants import Tag
            from psd_tools.psd.tagged_blocks import TaggedBlock, RawData

            block = TaggedBlock(
                key=Tag.LAYER_SECTION_DIVIDER,
                data=RawData(json.dumps(metadata).encode("utf-8")),
            )
            psd.tagged_blocks[block.key] = block
        except Exception:
            pass  # Metadata embedding is best-effort; don't fail the save

    psd.save(output_path)
    logger.info(f"PSD saved: {output_path} ({len(layers)+1} layers)")

    if create_thumb and self.path_object:
        # Build composite for thumbnail
        composite = base.copy()
        for ld in layers:
            img = ld.get("image")
            if img and ld.get("visible", True):
                overlay = img.convert("RGBA").resize(
                    (w, h), Image.Resampling.LANCZOS
                )
                composite = Image.alpha_composite(composite, overlay)
        self.img = composite
        self.create_file_thumb()

    return output_path.as_posix()

PNGImage

PNGImage(path: Union[str, Path, dict], **kwargs)

Bases: BaseImage

Source code in alc\file\images.py
def __init__(self, path: Union[str, Path, dict], **kwargs):
    super().__init__(path, **kwargs)

add_metadata staticmethod

add_metadata(
    data: dict, json_key: str = "alchemy_comfy"
) -> "PngImagePlugin.PngInfo"

Build PNG tEXt chunks from a metadata dict. Writes a standard SD-compatible 'parameters' key (readable by Civitai, A1111, ComfyUI image viewers) plus a JSON key with the full data for lossless roundtripping — alchemy_comfy for a ComfyUI render, alchemy_gen for a hosted-model generate (nano-banana). One writer, so every generated still in the pipeline carries its provenance the same way and any viewer reads them all.

Static so a caller that isn't holding a PNGImage — a service that just wrote bytes from an API — can still stamp through the one implementation.

Source code in alc\file\images.py
@staticmethod
def add_metadata(
    data: dict, json_key: str = "alchemy_comfy"
) -> "PngImagePlugin.PngInfo":
    """
    Build PNG tEXt chunks from a metadata dict.
    Writes a standard SD-compatible 'parameters' key (readable by Civitai,
    A1111, ComfyUI image viewers) plus a JSON key with the full data for
    lossless roundtripping — ``alchemy_comfy`` for a ComfyUI render,
    ``alchemy_gen`` for a hosted-model generate (nano-banana). One writer, so
    every generated still in the pipeline carries its provenance the same way
    and any viewer reads them all.

    Static so a caller that isn't holding a PNGImage — a service that just wrote
    bytes from an API — can still stamp through the one implementation.
    """

    png_info = PngImagePlugin.PngInfo()

    positive = data.get("positive", "")
    negative = data.get("negative", "")
    lora = data.get("lora", "")

    detail_fields = [
        ("steps", "Steps"),
        ("cfg", "CFG scale"),
        ("sampler", "Sampler"),
        ("denoise", "Denoise"),
        ("checkpoint", "Model"),
        ("workflow", "Workflow"),
        # WHERE this came from — the beat that asked for it and the address of
        # the thing it belongs to. In the human-readable chunk (not just the JSON)
        # so a plain file-properties dialog answers "what is this and whose is it".
        ("beat", "Beat"),
        ("source", "Source"),
    ]
    details = [
        f"{label}: {data[key]}" for key, label in detail_fields if data.get(key)
    ]
    if lora:
        details.append(f"Lora: {lora} ({data.get('lora_strength', '')})")

    parts = [positive] if positive else []
    if negative:
        parts.append(f"Negative prompt: {negative}")
    if details:
        parts.append(", ".join(details))

    if parts:
        png_info.add_text("parameters", "\n".join(parts))
    png_info.add_text(json_key, json.dumps(data))
    return png_info

save_png

save_png(
    output_path: Path = None,
    width: int = None,
    height: int = None,
    crop_box: CropBox = None,
    overwrite: bool = True,
    create_thumb: bool = True,
    metadata: dict = None,
) -> str

Override of BaseImage.save_png with optional metadata embedding.

Source code in alc\file\images.py
def save_png(
    self,
    output_path: Path = None,
    width: int = None,
    height: int = None,
    crop_box: CropBox = None,
    overwrite: bool = True,
    create_thumb: bool = True,
    metadata: dict = None,
) -> str:
    """Override of BaseImage.save_png with optional metadata embedding."""
    if not output_path:
        output_path = self.render_path
    output_path = Path(output_path)
    if not overwrite and output_path.exists():
        return output_path.as_posix()
    self.to_img()
    if crop_box:
        self.crop(crop_box.x, crop_box.y, crop_box.w, crop_box.h)
    if width or height:
        self.fit(width=width, height=height)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    png_info = self.add_metadata(metadata) if metadata else None
    save_kwargs = {"pnginfo": png_info} if png_info else {}
    self.img.save(output_path, format="PNG", **save_kwargs)
    if create_thumb and output_path.exists() and self.path_object:
        self.create_file_thumb()
    return output_path.as_posix()

generate_training_tiles

generate_training_tiles()

allows us to generate training tiles for a PNG image. typically used on background images for style training. Returns:

Source code in alc\file\images.py
def generate_training_tiles(self):
    """
    allows us to generate training tiles for a PNG image. typically used on background images for style training.
    Returns:

    """
    pass

is_svg_candidate

is_svg_candidate(
    *,
    min_transparency: float = 0.05,
    max_pixels: int = 4000000,
    max_colors: int = 600,
    top_coverage: float = 0.25
) -> Tuple[bool, str]

Heuristic: is this PNG 'logo-like' and worth vectorizing?

A good candidate floats on transparency (an icon/logo, not a full frame), isn't photo-resolution, and is colour-flat (few dominant colours cover most of it). The flatness test is what rejects transparent photographic cutouts (e.g. SAM object extractions) — those have alpha but spread their colour, and tracing them is pointless. Returns (ok, reason).

Source code in alc\file\images.py
def is_svg_candidate(
    self,
    *,
    min_transparency: float = 0.05,
    max_pixels: int = 4_000_000,
    max_colors: int = 600,
    top_coverage: float = 0.25,
) -> Tuple[bool, str]:
    """Heuristic: is this PNG 'logo-like' and worth vectorizing?

    A good candidate floats on transparency (an icon/logo, not a full frame),
    isn't photo-resolution, and is colour-flat (few dominant colours cover
    most of it). The flatness test is what rejects *transparent photographic
    cutouts* (e.g. SAM object extractions) — those have alpha but spread
    their colour, and tracing them is pointless. Returns ``(ok, reason)``.
    """
    img = self.to_img()
    if img.mode != "RGBA":
        return False, "no alpha channel (not a transparent logo)"
    w, h = img.size
    if w * h > max_pixels:
        return False, f"too large ({w}x{h}) — photo-res, not an icon"

    # Analyse a downsampled copy (NEAREST keeps colours crisp, bounds cost).
    analysis = img
    if max(w, h) > 256:
        scale = 256 / max(w, h)
        analysis = img.resize(
            (max(1, int(w * scale)), max(1, int(h * scale))), Image.NEAREST
        )
    rgb = list(analysis.convert("RGB").getdata())
    alpha = list(analysis.getchannel("A").getdata())
    opaque = [rgb[i] for i in range(len(rgb)) if alpha[i] >= 128]
    if not opaque:
        return False, "no opaque pixels"

    transparent_frac = 1 - (len(opaque) / len(rgb))
    if transparent_frac < min_transparency:
        return False, f"insufficient transparency ({transparent_frac:.0%})"

    from collections import Counter

    # Quantise to 5 bits/channel so anti-aliasing doesn't inflate the count.
    buckets = Counter((r >> 3, g >> 3, b >> 3) for r, g, b in opaque)
    coverage = sum(c for _, c in buckets.most_common(8)) / len(opaque)
    if len(buckets) > max_colors and coverage < top_coverage:
        return False, f"photographic ({len(buckets)} colours, top-8={coverage:.0%})"
    return True, "ok"

png_to_svg

png_to_svg(
    out_path: Union[str, Path] = None,
    *,
    force: bool = False,
    recolor: str = None,
    **vtracer_opts
) -> Optional[str]

Vectorise this PNG to a sidecar {name}.svg via vtracer (local).

Gated by :meth:is_svg_candidate unless force=True. vtracer respects the alpha channel, so transparent logos vectorise without a white box. recolor (a hex like "#D9D9D9") forces every fill to one colour — use it for dark marks that would vanish on a dark UI. Returns the svg path, or None if skipped. Extra kwargs pass through to vtracer.

Source code in alc\file\images.py
def png_to_svg(
    self,
    out_path: Union[str, Path] = None,
    *,
    force: bool = False,
    recolor: str = None,
    **vtracer_opts,
) -> Optional[str]:
    """Vectorise this PNG to a sidecar ``{name}.svg`` via vtracer (local).

    Gated by :meth:`is_svg_candidate` unless ``force=True``. vtracer respects
    the alpha channel, so transparent logos vectorise without a white box.
    ``recolor`` (a hex like ``"#D9D9D9"``) forces every fill to one colour —
    use it for dark marks that would vanish on a dark UI. Returns the svg
    path, or ``None`` if skipped. Extra kwargs pass through to vtracer.
    """
    ok, reason = (True, "forced") if force else self.is_svg_candidate()
    if not ok:
        logger.info(f"png_to_svg: skipping {self.path.name} — {reason}")
        return None

    out = Path(out_path) if out_path else self.path.with_suffix(".svg")
    if out.exists() and not force:
        return out.as_posix()

    try:
        import vtracer
    except ImportError:
        logger.error("png_to_svg: vtracer not installed (`pip install vtracer`)")
        return None

    opts = {"colormode": "color", "filter_speckle": 6, "color_precision": 7}
    opts.update(vtracer_opts)
    out.parent.mkdir(parents=True, exist_ok=True)
    vtracer.convert_image_to_svg_py(self.path.as_posix(), out.as_posix(), **opts)

    # vtracer emits fixed width/height and no viewBox — make it scale, and
    # optionally flatten to a single fill for dark-on-dark marks.
    self._finalize_svg(out, recolor=recolor)
    logger.info(f"png_to_svg: wrote {out}")
    return out.as_posix()

AlcImage

AlcImage(path: Union[str, Path, dict], **kwargs)

Bases: BaseImage

Source code in alc\file\images.py
def __init__(self, path: Union[str, Path, dict], **kwargs):
    super().__init__(path, **kwargs)

load

load() -> EXRImage | PNGImage | PSDImageFile | BaseImage

Inspects its own internal path configuration, determines the correct image subclass, and returns a fully instantiated, format-specific Pydantic model.

Source code in alc\file\images.py
def load(self) -> EXRImage | PNGImage | PSDImageFile | BaseImage:
    """
    Inspects its own internal path configuration, determines the correct image subclass,
    and returns a fully instantiated, format-specific Pydantic model.
    """
    # Read the current file extension off the class properties natively
    ext = self.path.suffix.lower()

    if ext == ".exr":
        return EXRImage(path=self.path)

    if ext == ".psd":
        return PSDImageFile(path=self.path)

    if ext in [".png", ".jpg", ".jpeg"]:
        return PNGImage(path=self.path)

    if ext == ".svg":
        # An SVG is an icon source in the AlcFile family — hand it to AlcIcon so
        # ``AlcFile.load()`` on a .svg can rasterize / resolve like any other icon.
        from alc.file.icon import AlcIcon

        return AlcIcon(path=self.path, source=self.path.as_posix(), kind="svg")

    return BaseImage(path=self.path)

Sequences

alc.file.sequences

Sequence

Bases: BaseModel

range_str property

range_str: str

Standard Alchemy range string: 1001-1100

validate_path_input classmethod

validate_path_input(data: any) -> any

Allows initialization via a string/Path or a full dict. If a string/Path is passed, it triggers the scanning logic.

Source code in alc\file\sequences.py
@model_validator(mode="before")
@classmethod
def validate_path_input(cls, data: any) -> any:
    """
    Allows initialization via a string/Path or a full dict.
    If a string/Path is passed, it triggers the scanning logic.
    """
    if isinstance(data, (str, Path)):
        return cls.parse_from_path(data)
    return data

from_path classmethod

from_path(path_or_pattern: str | Path) -> 'Sequence'

Build a Sequence from a concrete frame (cam.0001.png), a hash or printf pattern (cam.####.png / cam.%04d.png), or a directory. Thin alias over model_validate kept for historical call sites.

Source code in alc\file\sequences.py
@classmethod
def from_path(cls, path_or_pattern: str | Path) -> "Sequence":
    """Build a Sequence from a concrete frame (``cam.0001.png``), a hash or
    printf pattern (``cam.####.png`` / ``cam.%04d.png``), or a directory.
    Thin alias over ``model_validate`` kept for historical call sites."""
    return cls.model_validate(path_or_pattern)

parse_from_path classmethod

parse_from_path(path_or_pattern: str | Path) -> dict

Resolve any sequence reference into the model's fields, then scan the directory for the concrete frame range.

Accepts a concrete numbered frame (matched by _SEQ_NUM_RE) or a hash/printf pattern (handled by _infer_pattern).

Source code in alc\file\sequences.py
@classmethod
def parse_from_path(cls, path_or_pattern: str | Path) -> dict:
    """Resolve any sequence reference into the model's fields, then scan the
    directory for the concrete frame range.

    Accepts a concrete numbered frame (matched by ``_SEQ_NUM_RE``) or a
    hash/printf pattern (handled by ``_infer_pattern``)."""
    p = Path(path_or_pattern)

    m = _SEQ_NUM_RE.match(p.name)
    if m:
        head, frame, tail = m.group("head", "frame", "tail")
        ext = tail.split(" ", 1)[0]
        padding = len(frame)
        dir_ = p.parent
    else:
        dir_, head, padding, tail, ext = cls._infer_pattern(p)

    # Scan directory for frames
    frames = _scan_frames(dir_, head, ext)

    return {
        "dir": dir_,
        "head": head,
        "tail": tail,
        "padding": padding,
        "ext": ext,
        "frames": tuple(sorted(frames)),
    }

get_ffmpeg_cmd

get_ffmpeg_cmd(out_path: Path, fps: int = 24) -> List[str]

Build an ffmpeg command to encode this sequence to out_path.

Source code in alc\file\sequences.py
def get_ffmpeg_cmd(self, out_path: Path, fps: int = 24) -> List[str]:
    """Build an ffmpeg command to encode this sequence to ``out_path``."""
    cmd = [self._player_path("ffmpeg"), "-y", "-framerate", str(fps)]
    if self.start is not None:
        cmd += ["-start_number", str(self.start)]
    cmd += ["-i", self.printf_path.as_posix(), Path(out_path).as_posix()]
    return cmd

ffplay_cmd

ffplay_cmd(fps: int = 24) -> List[str]

Build an ffplay command to preview this sequence at fps.

Source code in alc\file\sequences.py
def ffplay_cmd(self, fps: int = 24) -> List[str]:
    """Build an ffplay command to preview this sequence at ``fps``."""
    cmd = [self._player_path("ffplay"), "-framerate", str(fps)]
    if self.start is not None:
        cmd += ["-start_number", str(self.start)]
    cmd += ["-i", self.printf_path.as_posix()]
    return cmd

AlcBaseSequence

AlcBaseSequence(path, **kwargs)

Bases: AlcFile

An image sequence on disk. path may be the sequence's directory, a representative frame (e.g. cam.0001.png), and frame logic is delegated to the :class:Sequence descriptor. Parallels BaseImage.

Source code in alc\file\sequences.py
def __init__(self, path, **kwargs):
    super().__init__(path, **kwargs)

frame_path

frame_path(frame_number: int = None) -> Path

Resolve the on-disk path of a specific frame (defaults to the first).

Source code in alc\file\sequences.py
def frame_path(self, frame_number: int = None) -> Path:
    """Resolve the on-disk path of a specific frame (defaults to the first)."""
    n = frame_number if frame_number is not None else self.start
    return Path(self.printf_path.as_posix() % n)

save_as_mp4

save_as_mp4(
    output_path: Path | str = None,
    fps: int = None,
    codec: str = "libx264",
    pix_fmt: str = "yuv420p",
    crf: int = 18,
    overwrite: bool = True,
) -> str

Encode the frame range to an .mp4. Thin wrapper — the actual ffmpeg work lives in alc.services.media.video (the conversion home).

Returns the output path as a posix string.

Source code in alc\file\sequences.py
def save_as_mp4(
    self,
    output_path: Path | str = None,
    fps: int = None,
    codec: str = "libx264",
    pix_fmt: str = "yuv420p",
    crf: int = 18,
    overwrite: bool = True,
) -> str:
    """Encode the frame range to an .mp4. Thin wrapper — the actual ffmpeg
    work lives in alc.services.media.video (the conversion home).

    Returns the output path as a posix string.
    """
    if not self.frames:
        raise FileNotFoundError(f"No frames to encode for {self.path}")

    from alc.services.media.video import sequence_to_mp4

    output_path = Path(output_path) if output_path else self._default_mp4_path()
    return sequence_to_mp4(
        printf_path=self.printf_path,
        output_path=output_path,
        start_frame=self.start,
        fps=fps or self.fps,
        codec=codec,
        pix_fmt=pix_fmt,
        crf=crf,
        overwrite=overwrite,
    )

make_proxy

make_proxy(overwrite: bool = True) -> dict

Render this sequence to a color-managed PNG proxy at the .proxy convention path ({res}/.proxy/{subpath}/{base}.####.png).

EXR sources go through oiiotool (linear->sRGB); everything else through ffmpeg. Returns the :func:exr_to_png / png_proxies result dict.

Source code in alc\file\sequences.py
def make_proxy(self, overwrite: bool = True) -> dict:
    """Render this sequence to a color-managed PNG proxy at the ``.proxy``
    convention path (``{res}/.proxy/{subpath}/{base}.####.png``).

    EXR sources go through oiiotool (linear->sRGB); everything else through
    ffmpeg. Returns the :func:`exr_to_png` / ``png_proxies`` result dict."""
    if not self.frames:
        raise FileNotFoundError(f"No frames to proxy for {self.path}")

    proxy_printf = Path(self.path_object.get_proxy_path(as_posix=False))
    proxy_dir = proxy_printf.parent

    if self.seq_ext.lower() == ".exr":
        from alc.services.media.oiio import exr_to_png

        return exr_to_png(
            input_printf=self.printf_path.as_posix(),
            output_dir=proxy_dir.as_posix(),
            start=self.start,
            end=self.end,
            overwrite=overwrite,
        )

    from alc.services.media.video import png_proxies

    return png_proxies(
        self.printf_path, output_dir=proxy_dir.as_posix(), overwrite=overwrite
    )

make_preview

make_preview(
    fps: int = None, overwrite: bool = True
) -> str

Encode an mp4 to the .preview convention path. PNG-currency first: if a proxy exists/can be built, movie from it (color-correct, normalized); else movie straight from the source frames.

Source code in alc\file\sequences.py
def make_preview(self, fps: int = None, overwrite: bool = True) -> str:
    """Encode an mp4 to the ``.preview`` convention path. PNG-currency first:
    if a proxy exists/can be built, movie from it (color-correct, normalized);
    else movie straight from the source frames."""
    try:
        proxy = self.make_proxy(overwrite=overwrite)
        printf = proxy["printf"]
        start = proxy["start"]
    except Exception:
        printf, start = self.printf_path.as_posix(), self.start

    from alc.services.media.video import sequence_to_mp4

    return sequence_to_mp4(
        printf_path=printf,
        output_path=self.path_object.get_preview_path(as_posix=False),
        start_frame=start,
        fps=fps or self.fps,
        overwrite=overwrite,
    )

make_thumb

make_thumb(
    frame: int = None, source_printf: str = None
) -> str

Write a still thumbnail to the .thumb convention path.

Thumbnails come from the color-managed PROXY PNGs by default, not the raw frames — grabbing a still straight from a linear EXR yields a dark/wrong image (no linear->sRGB). Pass source_printf to thumbnail a specific sequence; otherwise the existing .proxy is used, building it if absent.

Source code in alc\file\sequences.py
def make_thumb(self, frame: int = None, source_printf: str = None) -> str:
    """Write a still thumbnail to the ``.thumb`` convention path.

    Thumbnails come from the color-managed PROXY PNGs by default, not the raw
    frames — grabbing a still straight from a linear EXR yields a dark/wrong
    image (no linear->sRGB). Pass *source_printf* to thumbnail a specific
    sequence; otherwise the existing ``.proxy`` is used, building it if absent.
    """
    from alc.services.media.video import thumbnail

    src = source_printf
    if src is None:
        if self.seq_ext.lower() == ".exr":
            proxy = self.make_proxy(overwrite=False)
            src = proxy["printf"]
        else:
            src = self.printf_path.as_posix()

    return thumbnail(
        src,
        output_path=self.path_object.get_thumb_path(
            thumb_type="version", as_posix=False
        ),
        frame=frame,
    )

create_style_frame

create_style_frame(
    style_image,
    frame_number: int = None,
    provider: str = "nano_banana",
)

Generate a styled key frame for the sequence using style_image as the style reference, applied at frame_number (defaults to the first frame). Delegates to the chosen AI provider's create.style_frame and returns the styled frame (AlcImage).

Source code in alc\file\sequences.py
def create_style_frame(
    self, style_image, frame_number: int = None, provider: str = "nano_banana"
):
    """Generate a styled key frame for the sequence using ``style_image`` as
    the style reference, applied at ``frame_number`` (defaults to the first
    frame). Delegates to the chosen AI provider's create.style_frame and
    returns the styled frame (AlcImage).
    """
    from alc.services.ai import get_provider

    return get_provider(provider).create.style_frame(
        style_image, self.frame_path(frame_number)
    )

create_styled_movie

create_styled_movie(
    style_image,
    platform: str = "seedance",
    frame_number: int = None,
    **options
)

Two-part flow: (1) build a style frame from style_image at frame_number, then (2) hand that frame + this sequence/range to an AI video platform (default Seedance) to produce a styled movie. options carries per-platform settings.

Delegates to the platform provider's create.generate_video. (The seedance provider package is the next sibling to scaffold.)

Source code in alc\file\sequences.py
def create_styled_movie(
    self,
    style_image,
    platform: str = "seedance",
    frame_number: int = None,
    **options,
):
    """Two-part flow: (1) build a style frame from ``style_image`` at
    ``frame_number``, then (2) hand that frame + this sequence/range to an AI
    video platform (default Seedance) to produce a styled movie. ``options``
    carries per-platform settings.

    Delegates to the platform provider's create.generate_video. (The
    ``seedance`` provider package is the next sibling to scaffold.)
    """
    from alc.services.ai import get_provider

    style = self.create_style_frame(style_image, frame_number=frame_number)
    return get_provider(platform).create.generate_video(
        sequence=self, style_frame=style, **options
    )

AlcPngSequence

AlcPngSequence(path, **kwargs)

Bases: AlcBaseSequence

PNG image sequence. Parallels PNGImage. Inherits the base encode/style surface; override here for PNG-specific behavior (alpha, metadata) as needed.

Source code in alc\file\sequences.py
def __init__(self, path, **kwargs):
    super().__init__(path, **kwargs)

AlcSequence

AlcSequence(path, **kwargs)

Bases: AlcBaseSequence

Dispatcher mirroring AlcImage — resolves a sequence's concrete subclass from its frame extension.

Source code in alc\file\sequences.py
def __init__(self, path, **kwargs):
    super().__init__(path, **kwargs)