Skip to content

API Reference

App

App(customtkinter.CTk)

The main GUI window. Handles layout, file management, style selection, and export dispatch.

from vexy_lines_run import App

app = App()
app.mainloop()

Window defaults: Title "Style with Vexy Lines", 900x700, minimum 960x480.

Constructor: Takes no arguments. Builds the entire widget tree, registers drop targets, and configures initial state.

Key instance attributes:

Attribute Type Description
format_var tk.StringVar Current export format ("SVG", "PNG", "JPG", "MP4", "LINES")
size_var tk.StringVar Current size multiplier ("1x", "2x", "3x", "4x", or "—")
audio_var tk.BooleanVar Include audio in video exports
inputs_tabview CTkTabview The Lines/Images/Video tab container
styles_tabview CTkTabview The Style/End Style tab container
convert_button CTkButton The Export/Stop button

Internal state (not part of public API but useful to understand):

Attribute Type Description
_style_paths dict[str, str \| None] Maps "start"/"end" to loaded style file paths
_image_paths list[str] Loaded image file paths
_lines_paths list[str] Loaded .lines file paths
_video_path str Loaded video file path (empty if none)
_video_total_frames int Frame count of loaded video
_video_has_audio bool Whether loaded video has an audio stream
_video_range tuple[int, int] Selected frame range (1-indexed, inclusive)
_output_path str Last selected output directory or file path

launch() -> None

Creates an App and calls mainloop(). Sets CustomTkinter to dark appearance mode. The simplest way to start the GUI.

from vexy_lines_run import launch
launch()

Equivalent to:

import customtkinter
from vexy_lines_run import App

customtkinter.set_appearance_mode("dark")
app = App()
app.lift()
app.attributes("-topmost", True)
app.after(100, lambda: app.attributes("-topmost", False))
app.mainloop()

Processing

process_export(...) -> None

The export pipeline. Dispatches to mode-specific processors based on the mode parameter. Called by App on a background thread, but works fine called directly for scripting.

from vexy_lines_run.processing import process_export

process_export(
    mode="images",
    input_paths=["photo.jpg"],
    style_path="style.lines",
    end_style_path=None,
    output_path="./output",
    fmt="SVG",
    size="1x",
    audio=False,
    frame_range=None,
)

Parameters:

Parameter Type Default Description
mode str — "lines", "images", or "video"
input_paths list[str] — Paths to input files. For video mode, a single-element list with the video path.
style_path str \| None — Path to primary .lines style file, or None
end_style_path str \| None — Path to end .lines style file for interpolation, or None
output_path str — Output directory (or .mp4 filepath for video MP4 mode)
fmt str — "SVG", "PNG", "JPG", "MP4", or "LINES"
size str — "1x", "2x", "3x", "4x", or "—"
audio bool — Include audio track in MP4 output (keyword-only)
frame_range tuple[int, int] \| None — Start and end frame numbers (1-indexed, inclusive) for video mode. None means all frames. (keyword-only)
on_progress Callable[[int, int, str], None] \| None None Called with (current, total, message) after each file/frame
on_complete Callable[[str], None] \| None None Called with a summary message on success
on_error Callable[[str], None] \| None None Called with an error message on failure

Note: audio and frame_range are keyword-only parameters.

Mode behavior:

Mode Style required? What happens
"lines" + LINES No File copy to output directory
"lines" + PNG/JPG No Extracts embedded preview image from each .lines file
"lines" + SVG N/A Returns error: SVG export from .lines requires Images mode with a style
"images" Yes Applies style to each image via MCP. Each image → new Vexy Lines document → apply fill tree → render → export SVG → optionally rasterise
"video" + MP4 Yes Full video re-encode with per-frame style transfer. Audio passthrough when audio=True and source has audio and full range selected.
"video" + PNG/JPG/SVG Yes Extracts frames, styles each one, saves as frame_NNNNNN.ext

Callbacks are invoked on the calling thread. When App uses this function, it marshals them to the main Tk thread via self.after(0, callback).

Exceptions are caught internally and passed to on_error. The function does not raise.

Size multiplier parsing

The size string is parsed internally by _parse_size_multiplier():

Input Multiplier Effect
"1x" 1.0 Original resolution
"2x" 2.0 Double width and height (Lanczos resampling)
"3x" 3.0 Triple
"4x" 4.0 Quadruple
"—" 1.0 Same as 1x (used for SVG/LINES)

Values below 1.0 are clamped to 1.0.

Progress reporting

The on_progress callback receives three arguments:

  • current (int): 0-indexed count of processed items
  • total (int): total number of items to process
  • message (str): human-readable status, e.g. "Styling bear_03..." or "Frame 5/30" or "Done"

The final call is always (total, total, "Done") on success.

Error handling and recovery

All exceptions inside process_export() are caught and routed to on_error. The function never raises. Error messages include:

Error Cause
"No input files selected." Empty input_paths list
"A style file is required for Images mode." style_path is None in images mode
"A style file is required for Video mode." style_path is None in video mode
"Failed to read style: ..." Style .lines file can't be parsed
"Start and end styles have incompatible structures." End style doesn't match start style's fill tree
"MCP error: ...\n\nMake sure Vexy Lines is running." MCP connection refused or tool call failed
"SVG export from .lines files requires the Vexy Lines app (MCP)." SVG format selected in lines mode
"Unsupported format for Lines mode: ..." Invalid format for the mode

Video

Re-exported from vexy_lines_api.video. Video dependencies (OpenCV, svglab/resvg-py) must be installed.

VideoInfo

Dataclass with video file metadata.

from dataclasses import dataclass

@dataclass
class VideoInfo:
    width: int          # Frame width in pixels
    height: int         # Frame height in pixels
    fps: float          # Frames per second
    total_frames: int   # Total frame count
    duration: float     # Duration in seconds
    has_audio: bool     # Whether an audio stream exists

probe(path: str | Path) -> VideoInfo

Read video metadata without decoding any frames. Uses OpenCV's VideoCapture for dimensions/fps/frame count and ffprobe for audio detection. Returns instantly even for large files.

from vexy_lines_run.video import probe

info = probe("clip.mp4")
print(f"{info.width}x{info.height} @ {info.fps}fps")
print(f"{info.total_frames} frames, {info.duration:.1f}s")
print(f"Audio: {'yes' if info.has_audio else 'no'}")

Raises: - RuntimeError if the video file can't be opened - ImportError if opencv-python-headless is not installed

Audio detection: Uses ffprobe (best-effort). If ffprobe is not on PATH, has_audio returns False.

process_video(...) -> VideoInfo

Low-level video processing: decode frames, apply a single fill type per frame, re-encode. This is the standalone function that creates fills from scratch (fill_type + params), not from a .lines style file.

from vexy_lines_run.video import process_video

def frame_callback(frame_index, total_frames):
    progress = frame_index / max(total_frames - 1, 1)
    return {"angle": progress * 180.0, "interval": 15.0}

info = process_video(
    input_path="clip.mp4",
    output_path="result.mp4",
    fill_type="linear",
    color="#000000",
    interval=20,
    thickness=15,
    frame_params=frame_callback,
    max_frames=100,
    host="127.0.0.1",
    port=47384,
    dpi=72,
)

Parameters:

Parameter Type Default Description
input_path str \| Path — Source video file
output_path str \| Path — Destination video file
fill_type str "linear" Fill algorithm name
color str "#000000" Fill colour as hex string
interval float 20 Spacing between strokes in pixels
thickness float 15 Maximum stroke thickness
thickness_min float 0 Minimum stroke thickness
dpi int 72 Document DPI for the Vexy Lines engine
frame_params Callable[[int, int], dict] \| None None Called with (frame_index, total_frames), returns fill parameter overrides. When None, angle rotates from 0 to 180 degrees across the video.
max_frames int \| None None Limit processing to first N frames
on_progress Callable[[int, int], None] \| None None Called with (frame_index, total_frames) after each frame
host str "127.0.0.1" MCP server host
port int 47384 MCP server port
timeout float 60.0 Render timeout per frame in seconds

Returns: VideoInfo of the input video.

Raises: - ImportError if required packages are missing - MCPError if the MCP server is unreachable

process_video_with_style(...) -> VideoInfo

Higher-level video processing using extracted Style objects. Supports style interpolation across a frame range. This is what the GUI's processing pipeline uses internally.

from vexy_lines_run.video import process_video_with_style
from vexy_lines_api.style import extract_style

style = extract_style("halftone.lines")
end_style = extract_style("scribble.lines")

info = process_video_with_style(
    input_path="clip.mp4",
    output_path="result.mp4",
    style=style,
    end_style=end_style,
    start_frame=0,
    end_frame=150,
    dpi=72,
)

Parameters:

Parameter Type Default Description
input_path str \| Path — Source video file
output_path str \| Path — Destination video file
style Style — Start style object (from extract_style())
end_style Style \| None None End style for interpolation
dpi int 72 Document DPI
max_frames int \| None None Limit frames from start_frame
start_frame int 0 First frame to process (0-based, inclusive)
end_frame int \| None None Last frame to process (0-based, exclusive). None = all frames.
on_progress Callable[[int, int], None] \| None None Called with (frame_index, total_frames)
host str "127.0.0.1" MCP server host
port int 47384 MCP server port
timeout float 60.0 Render timeout per frame

Returns: VideoInfo of the input video.

Note: start_frame and end_frame are 0-based in this function, unlike the GUI which presents 1-indexed values to the user.


Widgets

CTkRangeSlider

A dual-handle range slider for CustomTkinter. Used in the Video tab to select frame ranges. Works as a standalone widget in any CustomTkinter app.

import customtkinter as ctk
from vexy_lines_run.widgets import CTkRangeSlider

slider = CTkRangeSlider(
    parent,
    from_=0,
    to=100,
    number_of_steps=100,
    command=lambda low, high: print(f"{low}-{high}"),
)
slider.pack(fill="x", padx=10)

Constructor parameters:

Parameter Type Default Description
master widget — Parent widget
width int 200 Widget width (horizontal) or 16 (vertical)
height int 16 Widget height (horizontal) or 200 (vertical)
corner_radius int auto Track corner radius
button_corner_radius int auto Thumb corner radius
border_width int 0 Track border width
button_length int auto Thumb width in pixels
fg_color str \| tuple theme Track background color
border_color str \| tuple theme Track border color
progress_color str \| tuple theme Color of the bar between thumbs
button_color str \| tuple theme Thumb color (or tuple for individual thumbs)
button_hover_color str \| tuple theme Thumb color on hover
from_ float 0 Minimum output value
to float 1 Maximum output value
number_of_steps int \| None None Quantize to this many steps (e.g. 100 for integer percentages)
state str "normal" "normal" or "disabled"
hover bool True Enable hover highlighting on thumbs
command callable \| None None Called with (low, high) on every value change
variables tuple[tk.Variable, tk.Variable] \| None None Bind to two Tk variables for two-way sync
orientation str "horizontal" "horizontal" or "vertical"

Methods:

Method Returns Description
get() tuple[float, float] Current (low, high) output values
set(values) — Set both values. Accepts [low, high] list.
configure(**kwargs) — Update any constructor parameter at runtime
cget(name) object Read a property value
bind(seq, cmd) — Bind an event handler (preserves internal bindings)
unbind(seq) — Remove an event handler
focus() — Give keyboard focus to the slider

Interaction behavior:

  • Click anywhere on the track to jump the nearest thumb to that position
  • Drag a thumb to slide it
  • Hovering over a thumb highlights it (if hover=True)
  • The slider prevents the low thumb from exceeding the high thumb and vice versa
  • With number_of_steps set, values snap to discrete positions

Theming: Follows CustomTkinter's dark/light appearance mode. All colors accept (light_value, dark_value) tuples for per-mode styling. The drawing engine auto-selects the best rendering backend for the platform (circle_shapes on macOS, font_shapes elsewhere).


Utility functions

These are internal to app.py but useful to understand:

extract_preview_from_lines(filepath: str) -> Image.Image | None

Extract the embedded preview image from a .lines file. Parses the XML, looks for the PreviewDoc element, decodes the base64 content. Returns a PIL Image or None if extraction fails (missing element, corrupt data, parse error).

extract_frame(video_path: str, frame_number: int) -> Image.Image | None

Extract a single frame from a video file using OpenCV. Frame numbers are 1-indexed. Returns a PIL Image in RGB mode, or None if extraction fails or OpenCV is not installed.

fit_image_to_box(image: Image.Image, width: int, height: int) -> Image.Image

Scale an image to fit within the given box while preserving aspect ratio. Uses Lanczos resampling. The result is pasted onto a dark canvas (#1d1f22) at position (0, 0), so the image sits in the top-left corner with dark padding on the right and/or bottom.

truncate_start(text: str, max_chars: int = 20) -> str

Trim leading characters, keeping only the last max_chars characters. Prepends "…" if truncated.

truncate_middle(text: str, max_width: int) -> str

Shorten text by replacing the middle with "⋮", keeping roughly equal amounts from the start and end.

create_placeholder_image(width: int, height: int, text: str) -> Image.Image

Returns a plain dark-grey (#1d1f22) image. The text parameter is accepted but not rendered: the placeholder is a solid color block.