| """ |
| Filename formatter for customizing output filenames using template syntax. |
| |
| Example usage: |
| from shared.utils.filename_formatter import FilenameFormatter |
| |
| template = "{date}-{prompt(50)}-{seed}" |
| settings = {"prompt": "A beautiful sunset over the ocean", "seed": 12345} |
| filename = FilenameFormatter.format_filename(template, settings) |
| # Result: "2025-01-15-14h30m45s-A_beautiful_sunset_over_the_ocean-12345" |
| |
| Date format examples: |
| {date} -> 2025-01-15-14h30m45s (default) |
| {date(YYYY-MM-DD)} -> 2025-01-15 |
| {date(YYYY/MM/DD)} -> 2025/01/15 |
| {date(DD.MM.YYYY)} -> 15.01.2025 |
| {date(YYYY-MM-DD_HH-mm-ss)} -> 2025-01-15_14-30-45 |
| {date(HHhmm)} -> 14h30 |
| """ |
|
|
| import re |
| import time |
| from datetime import datetime |
|
|
|
|
| class FilenameFormatter: |
| """ |
| Formats output filenames using template syntax with settings values. |
| |
| Supported placeholders: |
| - {date} - timestamp with default format YYYY-MM-DD-HHhmmss |
| - {date(YYYY-MM-DD)} - date with custom format and separator |
| - {date(YYYY-MM-DD_HH-mm-ss)} - date and time with custom separators |
| - {seed} - generation seed |
| - {resolution} - video resolution (e.g., "1280x720") |
| - {num_inference_steps} or {steps} - number of inference steps |
| - {prompt} or {prompt(50)} - prompt text with optional max length |
| - {flow_shift} - flow shift value |
| - {video_length} or {frames} - video length in frames |
| - {guidance_scale} or {cfg} - guidance scale value |
| |
| Date format tokens: |
| - YYYY: 4-digit year (2025) |
| - YY: 2-digit year (25) |
| - MM: 2-digit month (01-12) |
| - DD: 2-digit day (01-31) |
| - HH: 2-digit hour 24h (00-23) |
| - hh: 2-digit hour 12h (01-12) |
| - mm: 2-digit minute (00-59) |
| - ss: 2-digit second (00-59) |
| - Separators: - _ . : / and space |
| |
| Example templates: |
| - "{date}-{prompt(50)}-{seed}" |
| - "{date(YYYYMMDD)}-{resolution}-{steps}steps" |
| - "{date(YYYY-MM-DD_HH-mm-ss)}_{seed}" |
| """ |
|
|
| |
| ALLOWED_KEYS = { |
| 'date', 'seed', 'resolution', 'num_inference_steps', 'steps', |
| 'prompt', 'flow_shift', 'video_length', 'frames', |
| 'guidance_scale', 'cfg' |
| } |
|
|
| |
| KEY_ALIASES = { |
| 'steps': 'num_inference_steps', |
| 'frames': 'video_length', |
| 'cfg': 'guidance_scale' |
| } |
|
|
| |
| PLACEHOLDER_PATTERN = re.compile(r'\{(\w+)(?:\(([^)]*)\))?\}') |
|
|
| |
| DATE_TOKENS = [ |
| ('YYYY', '%Y'), |
| ('YY', '%y'), |
| ('MM', '%m'), |
| ('DD', '%d'), |
| ('HH', '%H'), |
| ('hh', '%I'), |
| ('mm', '%M'), |
| ('ss', '%S'), |
| ] |
|
|
| |
| DATE_SEPARATORS = set('-_.:/ h') |
|
|
| |
| UNSAFE_FILENAME_CHARS = re.compile(r'[<>:"/\\|?*\x00-\x1f\n\r\t/]') |
|
|
| def __init__(self, template: str): |
| """ |
| Initialize with a template string. |
| |
| Args: |
| template: Format string like "{date}-{prompt(50)}-{seed}" |
| |
| Raises: |
| ValueError: If template contains unknown placeholders |
| """ |
| self.template = template |
| self._validate_template() |
|
|
| def _validate_template(self): |
| """Validate that template only uses allowed placeholders.""" |
| for match in self.PLACEHOLDER_PATTERN.finditer(self.template): |
| key = match.group(1) |
| if key not in self.ALLOWED_KEYS: |
| allowed = ', '.join(sorted(self.ALLOWED_KEYS)) |
| raise ValueError(f"Unknown placeholder: {{{key}}}. Allowed: {allowed}") |
|
|
| def _parse_date_format(self, fmt: str) -> str: |
| """ |
| Convert user-friendly date format to strftime format. |
| |
| Args: |
| fmt: User format like "YYYY-MM-DD" or "YYYY/MM/DD_HH-mm-ss" |
| |
| Returns: |
| strftime format string like "%Y-%m-%d" or "%Y/%m/%d_%H-%M-%S" |
| """ |
| result = fmt |
|
|
| |
| for token, strftime_code in self.DATE_TOKENS: |
| result = result.replace(token, strftime_code) |
|
|
| return result |
|
|
| def _is_valid_date_format(self, fmt: str) -> bool: |
| """ |
| Check if date format string contains only valid tokens and separators. |
| |
| Args: |
| fmt: User format string to validate |
| |
| Returns: |
| True if format is valid and safe |
| """ |
| |
| remaining = fmt |
|
|
| |
| for token, _ in self.DATE_TOKENS: |
| remaining = remaining.replace(token, '') |
|
|
| |
| return all(c in self.DATE_SEPARATORS for c in remaining) |
|
|
| def _format_date(self, arg: str = None) -> str: |
| """ |
| Format current timestamp. |
| |
| Args: |
| arg: Optional date format string like "YYYY-MM-DD" or "HH:mm:ss" |
| If None or invalid, uses default format. |
| |
| Returns: |
| Formatted date/time string |
| """ |
| default_fmt = "%Y-%m-%d-%Hh%Mm%Ss" |
|
|
| if arg is None: |
| strftime_fmt = default_fmt |
| elif self._is_valid_date_format(arg): |
| strftime_fmt = self._parse_date_format(arg) |
| else: |
| |
| strftime_fmt = default_fmt |
|
|
| try: |
| return datetime.fromtimestamp(time.time()).strftime(strftime_fmt) |
| except Exception: |
| return datetime.fromtimestamp(time.time()).strftime(default_fmt) |
|
|
| def _truncate(self, value: str, max_len: int) -> str: |
| """Truncate string to max length.""" |
| if max_len <= 0 or len(value) <= max_len: |
| return value |
| return value[:max_len].rstrip() |
|
|
| def _sanitize_for_filename(self, value: str) -> str: |
| """ |
| Remove/replace characters unsafe for filenames. |
| |
| - Replaces unsafe chars with underscore |
| - Collapses multiple underscores/spaces |
| - Strips leading/trailing underscores and spaces |
| """ |
| if not value: |
| return '' |
|
|
| |
| sanitized = self.UNSAFE_FILENAME_CHARS.sub('_', str(value)) |
|
|
| |
| sanitized = re.sub(r'[_\s]+', '_', sanitized) |
|
|
| |
| return sanitized.strip('_ ') |
|
|
| def format(self, settings: dict) -> str: |
| """ |
| Format the template with settings values. |
| |
| Args: |
| settings: Dictionary containing settings values |
| |
| Returns: |
| Formatted filename (without extension), safe for filesystem |
| """ |
| def replace_placeholder(match): |
| key = match.group(1) |
| arg = match.group(2) |
|
|
| |
| if key == 'date': |
| return self._format_date(arg) |
|
|
| |
| actual_key = self.KEY_ALIASES.get(key, key) |
|
|
| |
| value = settings.get(actual_key) |
|
|
| |
| if value is None: |
| value = '' |
| else: |
| value = str(value) |
|
|
| |
| if arg is not None and arg.isdigit(): |
| max_len = int(arg) |
| value = self._truncate(value, max_len) |
|
|
| return self._sanitize_for_filename(value) |
|
|
| result = self.PLACEHOLDER_PATTERN.sub(replace_placeholder, self.template) |
|
|
| |
| result = self._sanitize_for_filename(result) |
|
|
| |
| if not result: |
| result = self._format_date() |
|
|
| return result |
|
|
| @classmethod |
| def format_filename(cls, template: str, settings: dict) -> str: |
| """ |
| Convenience class method to format a filename in one call. |
| |
| Args: |
| template: Format string like "{date}-{prompt(50)}-{seed}" |
| settings: Dictionary containing settings values |
| |
| Returns: |
| Formatted filename (without extension) |
| |
| Raises: |
| ValueError: If template contains unknown placeholders |
| """ |
| formatter = cls(template) |
| return formatter.format(settings) |
|
|
| @classmethod |
| def get_help_text(cls) -> str: |
| """Return help text describing the template syntax.""" |
| return """Filename Template Syntax: |
| |
| Placeholders (wrap in curly braces): |
| {date} - Timestamp (default: 2025-01-15-14h30m45s) |
| {date(YYYY-MM-DD)} - Date with custom format |
| {date(HH-mm-ss)} - Time only |
| {date(YYYY-MM-DD_HH-mm-ss)} - Date and time |
| {seed} - Generation seed |
| {resolution} - Video resolution (e.g., 1280x720) |
| {num_inference_steps} - Number of inference steps (alias: {steps}) |
| {prompt} - Full prompt text |
| {prompt(50)} - Prompt truncated to 50 characters |
| {flow_shift} - Flow shift value |
| {video_length} - Video length in frames (alias: {frames}) |
| {guidance_scale} - Guidance scale (alias: {cfg}) |
| |
| Date/Time tokens: |
| YYYY - 4-digit year MM - month (01-12) DD - day (01-31) |
| HH - hour 24h (00-23) hh - hour 12h (01-12) |
| mm - minute (00-59) ss - second (00-59) |
| Separators: - _ . : / space h |
| |
| Examples: |
| {date}-{prompt(50)}-{seed} |
| {date(YYYYMMDD)}_{resolution}_{steps}steps |
| {date(YYYY-MM-DD_HH-mm-ss)}_{seed} |
| {date(DD.MM.YYYY)}_{prompt(30)} |
| """ |
|
|