|
|
""" |
|
|
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)} |
|
|
""" |
|
|
|