```
```python
import json
import sys
import argparse
import glob
from collections import defaultdict
from typing import Dict, List, Tuple, Optional, Union
import numpy as np
import pandas as pd
from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder, StandardScaler
import lightgbm as lgb
import optuna
from optuna.integration import LightGBMPruningCallback
from tqdm import tqdm
import os
import joblib
import yaml

def load_json_file(file_path: str) -> Dict:
    """
    Loads and parses the JSON data from the specified file.

    Args:
        file_path (str): The path to the JSON file.

    Returns:
        Dict: Parsed JSON data.
    """
    try:
        with open(file_path, 'r', encoding='utf-8') as file:
            data = json.load(file)
        return data
    except FileNotFoundError:
        print(f"Error: File '{file_path}' not found.")
        sys.exit(1)
    except json.JSONDecodeError as e:
        print(f"Error: Failed to parse JSON. {e}")
        sys.exit(1)

def load_yaml_file(file_path: str) -> Dict:
    """
    Loads and parses the YAML data from the specified file.

    Args:
        file_path (str): The path to the YAML file.

    Returns:
        Dict: Parsed YAML data.
    """
    try:
        with open(file_path, 'r', encoding='utf-8') as file:
            data = yaml.safe_load(file)
        return data
    except FileNotFoundError:
        print(f"Error: YAML file '{file_path}' not found.")
        sys.exit(1)
    except yaml.YAMLError as e:
        print(f"Error: Failed to parse YAML. {e}")
        sys.exit(1)

def load_list_file(file_path: str) -> List[str]:
    """
    Loads a list of file paths from a text file.

    Args:
        file_path (str): The path to the list file.

    Returns:
        List[str]: List of file paths.
    """
    try:
        with open(file_path, 'r', encoding='utf-8') as file:
            files = [line.strip() for line in file if line.strip()]
        return files
    except FileNotFoundError:
        print(f"Error: List file '{file_path}' not found.")
        sys.exit(1)

def gather_json_files(input_paths: List[str]) -> List[str]:
    """
    Gathers all JSON file paths based on various input methods:
    - Direct file paths
    - Directory paths
    - Glob patterns
    - List files containing JSON paths

    Args:
        input_paths (List[str]): List of input paths.

    Returns:
        List[str]: Consolidated list of JSON file paths.
    """
    json_files = []
    for path in input_paths:
        if os.path.isdir(path):
            # Add all JSON files in the directory (recursively)
            dir_json_files = glob.glob(os.path.join(path, '**', '*.json'), recursive=True)
            json_files.extend(dir_json_files)
        elif '*' in path or '?' in path:
            # Handle glob patterns
            glob_files = glob.glob(path, recursive=True)
            json_files.extend([f for f in glob_files if f.endswith('.json')])
        elif path.lower().endswith('.txt'):
            # Assume it's a list file
            list_files = load_list_file(path)
            json_files.extend([f for f in list_files if f.endswith('.json')])
        elif path.lower().endswith(('.json')):
            # Direct JSON file
            json_files.append(path)
        else:
            print(f"Warning: Path '{path}' is not a JSON file, directory, glob pattern, or list file. Skipping.")
    return json_files

def extract_text_elements(json_data: Dict) -> pd.DataFrame:
    """
    Extracts text elements from Excalidraw JSON data into a pandas DataFrame.

    Args:
        json_data (Dict): Parsed Excalidraw JSON data.

    Returns:
        pd.DataFrame: DataFrame containing extracted text elements.
    """
    elements = json_data.get("elements", [])
    data = []
    for idx, element in enumerate(elements, start=1):
        if element.get("type") == "text":
            font_family = element.get("fontFamily")
            font_size = element.get("fontSize")
            width = element.get("width")
            height = element.get("height")
            text = element.get("text", "")
            text_length = len(text)
            is_multi_line = 1 if '\n' in text else 0
            text_align = element.get("textAlign")
            line_height = element.get("lineHeight")
            stroke_color = element.get("strokeColor")
            background_color = element.get("backgroundColor")
            fill_style = element.get("fillStyle")
            stroke_width = element.get("strokeWidth")
            opacity = element.get("opacity")
            angle = element.get("angle")
            element_id = element.get("id", f"Index_{idx}")
            # Additional features can be extracted here

            data.append({
                "id": element_id,
                "fontFamily": font_family,
                "fontSize": font_size,
                "text": text,
                "textLength": text_length,
                "isMultiLine": is_multi_line,
                "textAlign": text_align,
                "lineHeight": line_height,
                "strokeColor": stroke_color,
                "backgroundColor": background_color,
                "fillStyle": fill_style,
                "strokeWidth": stroke_width,
                "opacity": opacity,
                "angle": angle,
                "width": width,
                "height": height
            })
    df = pd.DataFrame(data)
    return df

def extract_adhoc_elements(input_elements: List[Dict[str, Union[str, int, float]]]) -> pd.DataFrame:
    """
    Extracts ad-hoc text elements from user-provided inputs into a pandas DataFrame.

    Args:
        input_elements (List[Dict]): List of text element dictionaries.

    Returns:
        pd.DataFrame: DataFrame containing extracted text elements.
    """
    data = []
    for idx, element in enumerate(input_elements, start=1):
        element_id = element.get("id", f"Input_Index_{idx}")
        font_family = element.get("fontFamily")
        font_size = element.get("fontSize")
        text = element.get("text", "")
        text_length = len(text)
        is_multi_line = 1 if '\n' in text else 0
        text_align = element.get("textAlign", "left")
        line_height = element.get("lineHeight", 1.25)
        stroke_color = element.get("strokeColor", "#000000")
        background_color = element.get("backgroundColor", "#FFFFFF")
        fill_style = element.get("fillStyle", "solid")
        stroke_width = element.get("strokeWidth", 1)
        opacity = element.get("opacity", 50)
        angle = element.get("angle", 0)
        # Note: Ad-hoc inputs may omit many fields; defaults are provided.

        data.append({
            "id": element_id,
            "fontFamily": font_family,
            "fontSize": font_size,
            "text": text,
            "textLength": text_length,
            "isMultiLine": is_multi_line,
            "textAlign": text_align,
            "lineHeight": line_height,
            "strokeColor": stroke_color,
            "backgroundColor": background_color,
            "fillStyle": fill_style,
            "strokeWidth": stroke_width,
            "opacity": opacity,
            "angle": angle,
            "width": np.nan,  # Width and height unknown for ad-hoc inputs
            "height": np.nan
        })
    df = pd.DataFrame(data)
    return df

def preprocess_data(df: pd.DataFrame, selected_features: List[str], encoders: Optional[Dict]=None, scalers: Optional[Dict]=None, fit: bool=True) -> Tuple[pd.DataFrame, Dict, Dict]:
    """
    Preprocesses the DataFrame by handling categorical variables and scaling numerical features.

    Args:
        df (pd.DataFrame): Input DataFrame.
        selected_features (List[str]): List of features to include.
        encoders (Dict, optional): Existing encoders for categorical features.
        scalers (Dict, optional): Existing scalers for numerical features.
        fit (bool, optional): Whether to fit the encoders and scalers.

    Returns:
        Tuple[pd.DataFrame, Dict, Dict]: Preprocessed DataFrame, encoders, and scalers.
    """
    # Define available features
    ALL_FEATURES = [
        'fontFamily',
        'fontSize',
        'textLength',
        'isMultiLine',
        'textAlign',
        'lineHeight',
        'strokeColor',
        'backgroundColor',
        'fillStyle',
        'strokeWidth',
        'opacity',
        'angle'
    ]

    # Validate selected features
    for feature in selected_features:
        if feature not in ALL_FEATURES:
            print(f"Warning: Feature '{feature}' is not recognized and will be ignored.")
    # Filter out unrecognized features
    selected_features = [feature for feature in selected_features if feature in ALL_FEATURES]

    if not selected_features:
        print("Error: No valid features selected for preprocessing.")
        sys.exit(1)

    categorical_features = [f for f in selected_features if f in ['fontFamily', 'textAlign', 'strokeColor', 'backgroundColor', 'fillStyle']]
    numerical_features = [f for f in selected_features if f in ['fontSize', 'textLength', 'isMultiLine', 'lineHeight', 'strokeWidth', 'opacity', 'angle']]

    if fit:
        # Initialize encoders and scalers
        encoders = {}
        scalers = {}
        for col in categorical_features:
            le = LabelEncoder()
            df[col] = df[col].fillna('Unknown')
            le.fit(df[col])
            encoders[col] = le
            df[col] = le.transform(df[col])
        if numerical_features:
            scaler = StandardScaler()
            df[numerical_features] = scaler.fit_transform(df[numerical_features])
            scalers['numerical'] = scaler
    else:
        # Use existing encoders and scalers
        if encoders is None or scalers is None:
            print("Error: Encoders and scalers must be provided when fit=False.")
            sys.exit(1)
        for col in categorical_features:
            le = encoders.get(col)
            if le:
                df[col] = df[col].fillna('Unknown')
                # Handle unseen labels
                df[col] = df[col].map(lambda s: s if s in le.classes_ else 'Unknown')
                if 'Unknown' not in le.classes_:
                    le.classes_ = np.append(le.classes_, 'Unknown')
                df[col] = le.transform(df[col])
            else:
                print(f"Error: Encoder for '{col}' not found.")
                sys.exit(1)
        if numerical_features:
            scaler = scalers.get('numerical')
            if scaler:
                df[numerical_features] = scaler.transform(df[numerical_features])
            else:
                print("Error: Scaler for numerical features not found.")
                sys.exit(1)

    return df, encoders, scalers

def optimize_lightgbm(train_x, train_y, valid_x, valid_y, params, trial):
    """
    Objective function for Optuna to optimize LightGBM hyperparameters.

    Args:
        train_x (pd.DataFrame): Training features.
        train_y (pd.Series): Training target.
        valid_x (pd.DataFrame): Validation features.
        valid_y (pd.Series): Validation target.
        params (Dict): Hyperparameters.
        trial (optuna.trial.Trial): Optuna trial object.

    Returns:
        float: Validation RMSE.
    """
    gbm = lgb.LGBMRegressor(**params)
    gbm.fit(
        train_x, train_y,
        eval_set=[(valid_x, valid_y)],
        eval_metric='rmse',
        early_stopping_rounds=50,
        verbose=False,
        callbacks=[LightGBMPruningCallback(trial, "rmse")]
    )
    preds = gbm.predict(valid_x)
    rmse = mean_squared_error(valid_y, preds, squared=False)
    return rmse

def train_model(df: pd.DataFrame, target: str, selected_features: List[str], encoders: Dict, scalers: Dict) -> Tuple[lgb.LGBMRegressor, Dict, Dict]:
    """
    Trains a LightGBM model to predict the specified target.

    Args:
        df (pd.DataFrame): Preprocessed DataFrame.
        target (str): The target variable ('width' or 'height').
        selected_features (List[str]): List of features to include.
        encoders (Dict): Encoders used during preprocessing.
        scalers (Dict): Scalers used during preprocessing.

    Returns:
        Tuple[lgb.LGBMRegressor, Dict, Dict]: Trained model, encoders, and scalers.
    """
    X = df[selected_features]
    y = df[target]

    # Split data
    X_train, X_valid, y_train, y_valid = train_test_split(
        X, y, test_size=0.2, random_state=42, shuffle=True
    )

    # Hyperparameter optimization using Optuna
    def objective(trial):
        param = {
            'objective': 'regression',
            'metric': 'rmse',
            'verbosity': -1,
            'boosting_type': 'gbdt',
            'learning_rate': trial.suggest_loguniform('learning_rate', 0.01, 0.3),
            'num_leaves': trial.suggest_int('num_leaves', 20, 300),
            'max_depth': trial.suggest_int('max_depth', 5, 50),
            'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
            'subsample': trial.suggest_uniform('subsample', 0.5, 1.0),
            'colsample_bytree': trial.suggest_uniform('colsample_bytree', 0.5, 1.0),
            'reg_alpha': trial.suggest_loguniform('reg_alpha', 1e-8, 10.0),
            'reg_lambda': trial.suggest_loguniform('reg_lambda', 1e-8, 10.0),
        }

        rmse = optimize_lightgbm(X_train, y_train, X_valid, y_valid, param, trial)
        return rmse

    study = optuna.create_study(direction='minimize')
    study.optimize(objective, n_trials=50, timeout=600)

    print(f"Best trial for {target}:")
    trial = study.best_trial
    print(f"  Value: {trial.value:.4f}")
    print(f"  Params:")
    for key, value in trial.params.items():
        print(f"    {key}: {value}")

    # Train final model with best parameters
    best_params = study.best_params
    best_params.update({'objective': 'regression', 'metric': 'rmse', 'boosting_type': 'gbdt', 'verbosity': -1})
    final_model = lgb.LGBMRegressor(**best_params)
    final_model.fit(
        X_train, y_train,
        eval_set=[(X_valid, y_valid)],
        eval_metric='rmse',
        early_stopping_rounds=50,
        verbose=False
    )

    return final_model, encoders, scalers

def evaluate_model(model: lgb.LGBMRegressor, X_test: pd.DataFrame, y_test: pd.Series) -> Dict:
    """
    Evaluates the model on test data.

    Args:
        model (lgb.LGBMRegressor): Trained LightGBM model.
        X_test (pd.DataFrame): Test features.
        y_test (pd.Series): Test target.

    Returns:
        Dict: Dictionary containing evaluation metrics.
    """
    preds = model.predict(X_test)
    mae = mean_absolute_error(y_test, preds)
    mse = mean_squared_error(y_test, preds)
    rmse = mean_squared_error(y_test, preds, squared=False)
    r2 = r2_score(y_test, preds)
    return {
        'MAE': mae,
        'MSE': mse,
        'RMSE': rmse,
        'R2': r2
    }

def save_model(model: lgb.LGBMRegressor, encoders: Dict, scalers: Dict, selected_features: List[str], file_path: str):
    """
    Saves the trained model along with encoders, scalers, and selected features.

    Args:
        model (lgb.LGBMRegressor): Trained LightGBM model.
        encoders (Dict): Encoders used during preprocessing.
        scalers (Dict): Scalers used during preprocessing.
        selected_features (List[str]): List of features used.
        file_path (str): Path to save the model.
    """
    model_dict = {
        'model': model,
        'encoders': encoders,
        'scalers': scalers,
        'selected_features': selected_features
    }
    joblib.dump(model_dict, file_path)
    print(f"Model successfully saved to '{file_path}'.")

def load_model(file_path: str) -> Tuple[lgb.LGBMRegressor, Dict, Dict, List[str]]:
    """
    Loads the trained model along with encoders, scalers, and selected features.

    Args:
        file_path (str): Path to the saved model.

    Returns:
        Tuple[lgb.LGBMRegressor, Dict, Dict, List[str]]: Loaded model, encoders, scalers, and selected features.
    """
    if not os.path.exists(file_path):
        print(f"Error: Model file '{file_path}' does not exist.")
        sys.exit(1)
    model_dict = joblib.load(file_path)
    return model_dict['model'], model_dict['encoders'], model_dict['scalers'], model_dict['selected_features']

def predict_dimensions(model: lgb.LGBMRegressor, encoders: Dict, scalers: Dict, selected_features: List[str], df: pd.DataFrame) -> np.ndarray:
    """
    Makes predictions using the trained model.

    Args:
        model (lgb.LGBMRegressor): Trained LightGBM model.
        encoders (Dict): Encoders used during preprocessing.
        scalers (Dict): Scalers used during preprocessing.
        selected_features (List[str]): List of features used.
        df (pd.DataFrame): DataFrame containing input features.

    Returns:
        np.ndarray: Predicted values.
    """
    X = df[selected_features].copy()

    # Preprocess data
    df_processed, _, _ = preprocess_data(X, selected_features, encoders, scalers, fit=False)

    predictions = model.predict(df_processed[selected_features])
    return predictions

def main():
    """
    Main function to execute training, testing, and prediction with subcommands:
    - train: Train models for width and height with selectable features.
    - test: Test trained models on multiple JSON files.
    - predict: Predict dimensions for a new JSON file or ad-hoc inputs.
    """
    parser = argparse.ArgumentParser(description="GBM-based Font Scale Analyzer for Excalidraw JSON files with Flexible Input Options.")
    subparsers = parser.add_subparsers(dest="command", required=True, help="Sub-commands")

    # Define all possible features
    AVAILABLE_FEATURES = [
        'fontFamily',
        'fontSize',
        'textLength',
        'isMultiLine',
        'textAlign',
        'lineHeight',
        'strokeColor',
        'backgroundColor',
        'fillStyle',
        'strokeWidth',
        'opacity',
        'angle'
    ]

    # Train subcommand
    train_parser = subparsers.add_parser("train", help="Train models for width and height predictions with selectable features.")
    train_parser.add_argument("input", type=str, nargs='+', help="Input path(s): JSON files, directories, glob patterns, or list files containing JSON paths.")
    train_parser.add_argument("--model_width", type=str, required=True, help="Path to save the trained width model.")
    train_parser.add_argument("--model_height", type=str, required=True, help="Path to save the trained height model.")
    train_parser.add_argument("--test_size", type=float, default=0.2, help="Proportion of data to include in the test split (default: 0.2).")
    train_parser.add_argument("--features", type=str, nargs='+', choices=AVAILABLE_FEATURES, help="List of features to include during training. If not specified, all features are used.")

    # Test subcommand
    test_parser = subparsers.add_parser("test", help="Test trained models on multiple JSON files.")
    test_parser.add_argument("model_width", type=str, help="Path to the trained width model.")
    test_parser.add_argument("model_height", type=str, help="Path to the trained height model.")
    test_parser.add_argument("input", type=str, nargs='+', help="Input path(s): JSON files, directories, glob patterns, or list files containing JSON paths.")

    # Predict subcommand
    predict_parser = subparsers.add_parser("predict", help="Predict width and height for a new JSON file or ad-hoc inputs.")
    predict_input_group = predict_parser.add_mutually_exclusive_group(required=True)
    predict_input_group.add_argument("--json", type=str, nargs='+', help="Path(s) to Excalidraw JSON file(s) for prediction.")
    predict_input_group.add_argument("--adhoc", type=str, help="Path to an external JSON or YAML file containing ad-hoc inputs for prediction.")
    predict_parser.add_argument("model_width", type=str, help="Path to the trained width model.")
    predict_parser.add_argument("model_height", type=str, help="Path to the trained height model.")
    predict_parser.add_argument("--output_csv", type=str, help="Path to save predictions as CSV (default: predictions.csv).", default="predictions.csv")

    args = parser.parse_args()

    if args.command == "train":
        input_paths = args.input
        model_width_path = args.model_width
        model_height_path = args.model_height
        test_size = args.test_size
        selected_features = args.features if args.features else AVAILABLE_FEATURES.copy()

        print(f"Selected Features for Training: {selected_features}\n")

        # Gather all JSON files based on input paths
        json_files = gather_json_files(input_paths)
        if not json_files:
            print("Error: No JSON files found based on the provided input paths.")
            sys.exit(1)

        # Load and combine data from multiple JSON files
        all_data = []
        for file in tqdm(json_files, desc="Loading JSON files"):
            json_data = load_json_file(file)
            df = extract_text_elements(json_data)
            all_data.append(df)
        combined_df = pd.concat(all_data, ignore_index=True)
        print(f"\nTotal text elements extracted: {len(combined_df)}")

        # Preprocess data
        print("\nPreprocessing data...")
        combined_df, encoders, scalers = preprocess_data(combined_df.copy(), selected_features, encoders=None, scalers=None, fit=True)

        # Train models for width and height
        print("\nTraining model for Width Prediction...")
        width_model, enc_width, scal_width = train_model(combined_df.copy(), 'width', selected_features, encoders.copy(), scalers.copy())

        print("\nTraining model for Height Prediction...")
        height_model, enc_height, scal_height = train_model(combined_df.copy(), 'height', selected_features, encoders.copy(), scalers.copy())

        # Save models
        print("\nSaving trained models...")
        save_model(width_model, enc_width, scal_width, selected_features, model_width_path)
        save_model(height_model, enc_height, scal_height, selected_features, model_height_path)

        print("\nTraining completed successfully.")

    elif args.command == "test":
        model_width_path = args.model_width
        model_height_path = args.model_height
        input_paths = args.input

        # Load models
        print("Loading trained models...")
        width_model, enc_width, scal_width, features_width = load_model(model_width_path)
        height_model, enc_height, scal_height, features_height = load_model(model_height_path)

        # Ensure that both models use the same features
        if features_width != features_height:
            print("Error: Width and Height models were trained with different feature sets.")
            sys.exit(1)
        selected_features = features_width
        print(f"\nSelected Features for Testing: {selected_features}\n")

        # Gather all JSON files based on input paths
        json_files = gather_json_files(input_paths)
        if not json_files:
            print("Error: No JSON files found based on the provided input paths.")
            sys.exit(1)

        # Load and combine test data
        all_test_data = []
        for file in tqdm(json_files, desc="Loading Test JSON files"):
            json_data = load_json_file(file)
            df = extract_text_elements(json_data)
            all_test_data.append(df)
        test_df = pd.concat(all_test_data, ignore_index=True)
        print(f"\nTotal test text elements extracted: {len(test_df)}")

        # Preprocess test data
        print("\nPreprocessing test data...")
        test_df_processed, _, _ = preprocess_data(test_df.copy(), selected_features, encoders=enc_width, scalers=scal_width, fit=False)

        # Split into features and targets
        X_test = test_df_processed[selected_features]
        y_test_width = test_df['width']
        y_test_height = test_df['height']

        # Evaluate width model
        print("\nEvaluating Width Model...")
        width_metrics = evaluate_model(width_model, X_test, y_test_width)
        print(f"Width Prediction Metrics:")
        for metric, value in width_metrics.items():
            print(f"  {metric}: {value:.4f}")

        # Evaluate height model
        print("\nEvaluating Height Model...")
        height_metrics = evaluate_model(height_model, X_test, y_test_height)
        print(f"Height Prediction Metrics:")
        for metric, value in height_metrics.items():
            print(f"  {metric}: {value:.4f}")

    elif args.command == "predict":
        model_width_path = args.model_width
        model_height_path = args.model_height
        output_csv = args.output_csv

        # Load models
        print("Loading trained models...")
        width_model, enc_width, scal_width, features_width = load_model(model_width_path)
        height_model, enc_height, scal_height, features_height = load_model(model_height_path)

        # Ensure that both models use the same features
        if features_width != features_height:
            print("Error: Width and Height models were trained with different feature sets.")
            sys.exit(1)
        selected_features = features_width
        print(f"\nSelected Features for Prediction: {selected_features}\n")

        # Handle input: either --json or --adhoc
        if args.json:
            input_paths = args.json
            # Gather all JSON files based on input paths
            json_files = gather_json_files(input_paths)
            if not json_files:
                print("Error: No JSON files found based on the provided input paths.")
                sys.exit(1)

            # Load and combine input data
            all_input_data = []
            for file in tqdm(json_files, desc="Loading Input JSON files"):
                json_data = load_json_file(file)
                df = extract_text_elements(json_data)
                all_input_data.append(df)
            input_df = pd.concat(all_input_data, ignore_index=True)
            print(f"\nTotal input text elements extracted: {len(input_df)}")

        elif args.adhoc:
            adhoc_path = args.adhoc
            # Determine file type based on extension
            if adhoc_path.lower().endswith('.json'):
                adhoc_data = load_json_file(adhoc_path)
            elif adhoc_path.lower().endswith(('.yaml', '.yml')):
                adhoc_data = load_yaml_file(adhoc_path)
            else:
                print("Error: Ad-hoc input file must be a JSON or YAML file.")
                sys.exit(1)
            # Expecting a list of text element dictionaries
            if isinstance(adhoc_data, list):
                input_df = extract_adhoc_elements(adhoc_data)
            elif isinstance(adhoc_data, dict):
                # Single element provided as a dict
                input_df = extract_adhoc_elements([adhoc_data])
            else:
                print("Error: Ad-hoc input should be a list of dictionaries or a single dictionary.")
                sys.exit(1)
            print(f"\nTotal ad-hoc text elements extracted: {len(input_df)}")

        else:
            print("Error: Either --json or --adhoc must be provided for prediction.")
            sys.exit(1)

        # Preprocess input data
        print("\nPreprocessing input data...")
        input_df_processed, _, _ = preprocess_data(input_df.copy(), selected_features, encoders=enc_width, scalers=scal_width, fit=False)

        # Make predictions
        print("\nMaking predictions...")
        if args.json:
            # Predictions for Excalidraw JSON inputs
            predicted_width = predict_dimensions(width_model, enc_width, scal_width, selected_features, input_df)
            predicted_height = predict_dimensions(height_model, enc_height, scal_height, selected_features, input_df)
        elif args.adhoc:
            # Predictions for ad-hoc inputs
            predicted_width = predict_dimensions(width_model, enc_width, scal_width, selected_features, input_df)
            predicted_height = predict_dimensions(height_model, enc_height, scal_height, selected_features, input_df)

        # Prepare output
        input_df['predicted_width'] = predicted_width
        input_df['predicted_height'] = predicted_height

        # For Excalidraw JSON, preserve the original 'id' if present
        # For ad-hoc inputs, use existing 'id' or generated ones

        # Save predictions to CSV
        input_df.to_csv(output_csv, index=False)
        print(f"\nPredictions saved to '{output_csv}'.")

    else:
        print("Invalid command. Use 'train', 'test', or 'predict'.")
        sys.exit(1)

if __name__ == "__main__":
    main()
```
anaylze what this script does