Made it a pip project.

This commit is contained in:
Steve White 2025-03-28 17:51:26 -05:00
commit 1a8cae62ca
24 changed files with 1065 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.DS_Store
.venv/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
MANIFEST.in Normal file
View File

@ -0,0 +1,3 @@
include README.md
include LICENSE
include src/pynamer/config.yaml

123
README.md Normal file
View File

@ -0,0 +1,123 @@
# PyNamer
PyNamer is a command-line tool that uses AI vision models to generate descriptive filenames for images. It analyzes the content of images and renames them with meaningful, descriptive filenames in snake_case format.
## Features
- Uses LiteLLM to integrate with various vision-capable LLMs (default: GPT-4 Vision)
- Configurable via YAML config file
- Supports multiple image formats (jpg, jpeg, png, gif, webp)
- Dry-run mode to preview changes without renaming files
- Handles filename collisions automatically
## Installation
### Option 1: Install from PyPI (recommended)
```bash
pip install pynamer
```
### Option 2: Install from source
1. Clone this repository
2. Install the package in development mode:
```bash
pip install -e .
```
### Set up your API key
You need to set up your API key for the vision model:
- Set the appropriate environment variable (e.g., `OPENAI_API_KEY`), or
- Create a custom config file with your API key
## Configuration
PyNamer uses the following configuration file locations (in order of precedence):
1. Custom config file specified with `-c` or `--config` option
2. User config file at `~/.config/pynamer.yaml` (created automatically on first run)
3. Default config file included with the package
You can customize the following settings:
- LLM provider and model
- API key and endpoint
- Supported image formats
- Prompt templates for filename generation
Example configuration file:
```yaml
llm:
provider: "openai"
model: "gpt-4-vision-preview"
api_key: "your-api-key-here"
max_tokens: 100
temperature: 0.7
```
## Usage
After installation, you can use PyNamer directly from the command line:
Basic usage:
```bash
pynamer path/to/image.jpg
```
Process multiple images:
```bash
pynamer image1.jpg image2.png image3.jpg
```
Use a different config file:
```bash
pynamer -c custom_config.yaml image.jpg
```
Preview changes without renaming (dry run):
```bash
pynamer -d image.jpg
```
Enable verbose logging:
```bash
pynamer -v image.jpg
```
## Example
Input: `IMG_20230615_123456.jpg` (a photo of a cat sleeping on a window sill)
Output: `orange_cat_sleeping_on_sunny_windowsill.jpg`
## Development
### Building the package
```bash
pip install build
python -m build
```
### Installing in development mode
```bash
pip install -e .
```
## Requirements
- Python 3.7+
- LiteLLM
- PyYAML
- Access to a vision-capable LLM API (OpenAI, Anthropic, etc.)

24
config.yaml Normal file
View File

@ -0,0 +1,24 @@
# PyNamer Configuration
# LLM API Configuration
llm:
provider: "openai" # Provider name (openai, anthropic, etc.)
model: "gpt-4o-mini" # Model name
api_key: "" # Your API key (leave empty to use environment variable)
endpoint: "" # Custom endpoint URL (if using a proxy or alternative service)
max_tokens: 100 # Maximum tokens for response
temperature: 0.7 # Temperature for generation
# Image Processing
image:
supported_formats:
- ".jpg"
- ".jpeg"
- ".png"
- ".gif"
- ".webp"
# Prompt Configuration
prompt:
system_message: "You are a helpful assistant that generates concise, descriptive filenames for images. Focus on the main subject, key attributes, and context. Use snake_case format without special characters."
user_message: "Generate a descriptive filename for this image. The filename should be concise, specific, and use snake_case format. Do not include a file extension."

274
pynamer.py Executable file
View File

@ -0,0 +1,274 @@
#!/Volumes/SAM2/CODE/pynamer/.venv/bin/python
import argparse
import base64
import os
import sys
from pathlib import Path
import yaml
from typing import Dict, List, Optional, Union
import litellm
from litellm import completion
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('pynamer')
class PyNamer:
"""A tool to generate descriptive filenames for images using LLMs."""
def __init__(self, config_path: str = 'config.yaml'):
"""Initialize the PyNamer with configuration.
Args:
config_path: Path to the YAML configuration file
"""
self.config = self._load_config(config_path)
self._setup_llm()
def _load_config(self, config_path: str) -> Dict:
"""Load configuration from YAML file.
Args:
config_path: Path to the configuration file
Returns:
Dict containing configuration
"""
try:
with open(config_path, 'r') as f:
config = yaml.safe_load(f)
logger.info(f"Loaded configuration from {config_path}")
return config
except Exception as e:
logger.error(f"Failed to load configuration: {e}")
sys.exit(1)
def _setup_llm(self) -> None:
"""Set up the LLM client based on configuration."""
llm_config = self.config.get('llm', {})
# Set API key if provided in config
api_key = llm_config.get('api_key')
if api_key:
os.environ["OPENAI_API_KEY"] = api_key
# Set custom endpoint if provided
endpoint = llm_config.get('endpoint')
if endpoint:
os.environ["OPENAI_API_BASE"] = endpoint
self.model = llm_config.get('model', 'gpt-4-vision-preview')
self.max_tokens = llm_config.get('max_tokens', 100)
self.temperature = llm_config.get('temperature', 0.7)
logger.info(f"LLM setup complete. Using model: {self.model}")
def _encode_image(self, image_path: str) -> str:
"""Encode image to base64 for API submission.
Args:
image_path: Path to the image file
Returns:
Base64 encoded image string
"""
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
def _is_supported_format(self, file_path: str) -> bool:
"""Check if the file format is supported.
Args:
file_path: Path to the file
Returns:
True if supported, False otherwise
"""
supported_formats = self.config.get('image', {}).get('supported_formats', [])
file_ext = os.path.splitext(file_path)[1].lower()
return file_ext in supported_formats
def generate_filename(self, image_path: str) -> str:
"""Generate a descriptive filename for the image using LLM.
Args:
image_path: Path to the image file
Returns:
Generated filename (without extension)
"""
if not os.path.exists(image_path):
logger.error(f"Image not found: {image_path}")
return None
if not self._is_supported_format(image_path):
logger.error(f"Unsupported file format: {image_path}")
return None
try:
# Encode image
base64_image = self._encode_image(image_path)
# Prepare messages for LLM
system_message = self.config.get('prompt', {}).get('system_message', '')
user_message = self.config.get('prompt', {}).get('user_message', '')
messages = [
{"role": "system", "content": system_message},
{
"role": "user",
"content": [
{"type": "text", "text": user_message},
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}
}
]
}
]
# Call LLM
response = completion(
model=self.model,
messages=messages,
max_tokens=self.max_tokens,
temperature=self.temperature
)
# Extract filename from response
filename = response.choices[0].message.content.strip()
logger.info(f"Generated filename: {filename}")
return filename
except Exception as e:
logger.error(f"Error generating filename: {e}")
return None
def rename_image(self, image_path: str, dry_run: bool = False) -> Optional[str]:
"""Rename the image with a generated descriptive filename.
Args:
image_path: Path to the image file
dry_run: If True, don't actually rename the file
Returns:
New path if successful, None otherwise
"""
# Generate filename
new_filename = self.generate_filename(image_path)
if not new_filename:
return None
# Clean up the filename (ensure snake_case, no special chars)
new_filename = new_filename.lower()
new_filename = ''.join(c if c.isalnum() else '_' for c in new_filename)
new_filename = new_filename.replace('__', '_').strip('_')
# Get original path components
path = Path(image_path)
directory = path.parent
extension = path.suffix
# Create new path
new_path = directory / f"{new_filename}{extension}"
# Rename file
if not dry_run:
try:
# Handle case where the new filename already exists
counter = 1
while new_path.exists():
new_path = directory / f"{new_filename}_{counter}{extension}"
counter += 1
path.rename(new_path)
logger.info(f"Renamed: {image_path} -> {new_path}")
except Exception as e:
logger.error(f"Error renaming file: {e}")
return None
else:
logger.info(f"[DRY RUN] Would rename: {image_path} -> {new_path}")
return str(new_path)
def main():
"""Main entry point for the script."""
parser = argparse.ArgumentParser(description='Generate descriptive filenames for images using LLMs')
parser.add_argument('images', nargs='+', help='Paths to image files')
parser.add_argument('-c', '--config', default='config.yaml', help='Path to configuration file')
parser.add_argument('-d', '--dry-run', action='store_true', help='Preview changes without renaming files')
parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output')
args = parser.parse_args()
# Set logging level
if args.verbose:
logger.setLevel(logging.DEBUG)
# Initialize PyNamer
namer = PyNamer(config_path=args.config)
# Process each image - handle image paths that might contain spaces
image_paths = []
# First, try to handle the case where the entire argument list is a single path with spaces
if len(args.images) > 1:
combined_path = ' '.join(args.images)
if os.path.exists(combined_path):
image_paths = [combined_path]
logger.info(f"Found image by combining all arguments: {combined_path}")
# If that didn't work, try to handle each argument individually
if not image_paths:
# Use a set to avoid duplicate processing
processed_paths = set()
for path in args.images:
# Check if the path exists as is
if os.path.exists(path) and path not in processed_paths:
image_paths.append(path)
processed_paths.add(path)
else:
# Try to find files that match the pattern
import glob
matching_files = glob.glob(f"*{path}*")
for file in matching_files:
if file not in processed_paths:
image_paths.append(file)
processed_paths.add(file)
if not matching_files:
logger.debug(f"Could not find any file matching '{path}'")
# Process each valid image path
if not image_paths:
print("Error: No valid image files found to process.")
return
# Remove duplicates while preserving order
unique_image_paths = []
seen = set()
for path in image_paths:
if path not in seen:
unique_image_paths.append(path)
seen.add(path)
# Process each image
for image_path in unique_image_paths:
if not os.path.exists(image_path):
logger.warning(f"Skipping non-existent file: {image_path}")
continue
new_path = namer.rename_image(image_path, dry_run=args.dry_run)
if new_path:
print(f"{'[DRY RUN] ' if args.dry_run else ''}Renamed: {image_path} -> {new_path}")
else:
print(f"Failed to process: {image_path}")
if __name__ == "__main__":
main()

11
pyproject.toml Normal file
View File

@ -0,0 +1,11 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 88
target-version = ['py37', 'py38', 'py39', 'py310']
[tool.isort]
profile = "black"
line_length = 88

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
litellm>=1.10.0
pyyaml>=6.0

50
setup.py Normal file
View File

@ -0,0 +1,50 @@
from setuptools import setup, find_packages
import os
# Read the contents of README.md
with open("README.md", encoding="utf-8") as f:
long_description = f.read()
# Read version from __init__.py
with open(os.path.join("src", "pynamer", "__init__.py"), encoding="utf-8") as f:
for line in f:
if line.startswith("__version__"):
version = line.split("=")[1].strip().strip('"').strip("'")
break
setup(
name="pynamer",
version=version,
description="Generate descriptive filenames for images using LLMs",
long_description=long_description,
long_description_content_type="text/markdown",
author="Your Name",
author_email="your.email@example.com",
url="https://github.com/yourusername/pynamer",
package_dir={"": "src"},
packages=find_packages(where="src"),
include_package_data=True,
package_data={
"pynamer": ["config.yaml"],
},
install_requires=[
"litellm>=1.10.0",
"pyyaml>=6.0",
],
python_requires=">=3.7",
entry_points={
"console_scripts": [
"pynamer=pynamer.cli:main",
],
},
classifiers=[
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
],
)

View File

@ -0,0 +1,138 @@
Metadata-Version: 2.1
Name: pynamer
Version: 0.1.0
Summary: Generate descriptive filenames for images using LLMs
Home-page: https://github.com/yourusername/pynamer
Author: Your Name
Author-email: your.email@example.com
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: litellm>=1.10.0
Requires-Dist: pyyaml>=6.0
# PyNamer
PyNamer is a command-line tool that uses AI vision models to generate descriptive filenames for images. It analyzes the content of images and renames them with meaningful, descriptive filenames in snake_case format.
## Features
- Uses LiteLLM to integrate with various vision-capable LLMs (default: GPT-4 Vision)
- Configurable via YAML config file
- Supports multiple image formats (jpg, jpeg, png, gif, webp)
- Dry-run mode to preview changes without renaming files
- Handles filename collisions automatically
## Installation
### Option 1: Install from PyPI (recommended)
```bash
pip install pynamer
```
### Option 2: Install from source
1. Clone this repository
2. Install the package in development mode:
```bash
pip install -e .
```
### Set up your API key
You need to set up your API key for the vision model:
- Set the appropriate environment variable (e.g., `OPENAI_API_KEY`), or
- Create a custom config file with your API key
## Configuration
PyNamer comes with a default configuration, but you can create a custom config file to customize:
- LLM provider and model
- API key and endpoint
- Supported image formats
- Prompt templates for filename generation
Example custom config file (config.yaml):
```yaml
llm:
provider: "openai"
model: "gpt-4-vision-preview"
api_key: "your-api-key-here"
max_tokens: 100
temperature: 0.7
```
## Usage
After installation, you can use PyNamer directly from the command line:
Basic usage:
```bash
pynamer path/to/image.jpg
```
Process multiple images:
```bash
pynamer image1.jpg image2.png image3.jpg
```
Use a different config file:
```bash
pynamer -c custom_config.yaml image.jpg
```
Preview changes without renaming (dry run):
```bash
pynamer -d image.jpg
```
Enable verbose logging:
```bash
pynamer -v image.jpg
```
## Example
Input: `IMG_20230615_123456.jpg` (a photo of a cat sleeping on a window sill)
Output: `orange_cat_sleeping_on_sunny_windowsill.jpg`
## Development
### Building the package
```bash
pip install build
python -m build
```
### Installing in development mode
```bash
pip install -e .
```
## Requirements
- Python 3.7+
- LiteLLM
- PyYAML
- Access to a vision-capable LLM API (OpenAI, Anthropic, etc.)

View File

@ -0,0 +1,15 @@
LICENSE
MANIFEST.in
README.md
pyproject.toml
setup.py
src/pynamer/__init__.py
src/pynamer/cli.py
src/pynamer/config.yaml
src/pynamer/core.py
src/pynamer.egg-info/PKG-INFO
src/pynamer.egg-info/SOURCES.txt
src/pynamer.egg-info/dependency_links.txt
src/pynamer.egg-info/entry_points.txt
src/pynamer.egg-info/requires.txt
src/pynamer.egg-info/top_level.txt

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,2 @@
[console_scripts]
pynamer = pynamer.cli:main

View File

@ -0,0 +1,2 @@
litellm>=1.10.0
pyyaml>=6.0

View File

@ -0,0 +1 @@
pynamer

3
src/pynamer/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""PyNamer - Generate descriptive filenames for images using LLMs."""
__version__ = "0.1.0"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

103
src/pynamer/cli.py Normal file
View File

@ -0,0 +1,103 @@
"""Command-line interface for PyNamer."""
import argparse
import os
import glob
import logging
from pathlib import Path
from typing import List, Optional
from .core import PyNamer, logger
from .setup_utils import ensure_user_config_exists
def find_image_files(args_images: List[str]) -> List[str]:
"""Find image files from command line arguments.
Args:
args_images: List of image paths from command line
Returns:
List of valid image paths
"""
image_paths = []
# First, try to handle the case where the entire argument list is a single path with spaces
if len(args_images) > 1:
combined_path = ' '.join(args_images)
if os.path.exists(combined_path):
image_paths = [combined_path]
logger.info(f"Found image by combining all arguments: {combined_path}")
# If that didn't work, try to handle each argument individually
if not image_paths:
# Use a set to avoid duplicate processing
processed_paths = set()
for path in args_images:
# Check if the path exists as is
if os.path.exists(path) and path not in processed_paths:
image_paths.append(path)
processed_paths.add(path)
else:
# Try to find files that match the pattern
matching_files = glob.glob(f"*{path}*")
for file in matching_files:
if file not in processed_paths:
image_paths.append(file)
processed_paths.add(file)
if not matching_files:
logger.debug(f"Could not find any file matching '{path}'")
# Remove duplicates while preserving order
unique_image_paths = []
seen = set()
for path in image_paths:
if path not in seen:
unique_image_paths.append(path)
seen.add(path)
return unique_image_paths
def main():
"""Main entry point for the script."""
# Ensure the user config exists
ensure_user_config_exists()
parser = argparse.ArgumentParser(description='Generate descriptive filenames for images using LLMs')
parser.add_argument('images', nargs='+', help='Paths to image files')
parser.add_argument('-c', '--config', help='Path to configuration file')
parser.add_argument('-d', '--dry-run', action='store_true', help='Preview changes without renaming files')
parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output')
args = parser.parse_args()
# Set logging level
if args.verbose:
logger.setLevel(logging.DEBUG)
# Initialize PyNamer
namer = PyNamer(config_path=args.config)
# Find image files
image_paths = find_image_files(args.images)
# Process each valid image path
if not image_paths:
print("Error: No valid image files found to process.")
return
# Process each image
for image_path in image_paths:
if not os.path.exists(image_path):
logger.warning(f"Skipping non-existent file: {image_path}")
continue
new_path = namer.rename_image(image_path, dry_run=args.dry_run)
if new_path:
print(f"{'[DRY RUN] ' if args.dry_run else ''}Renamed: {image_path} -> {new_path}")
else:
print(f"Failed to process: {image_path}")
if __name__ == "__main__":
main()

24
src/pynamer/config.yaml Normal file
View File

@ -0,0 +1,24 @@
# PyNamer Configuration
# LLM API Configuration
llm:
provider: "openai" # Provider name (openai, anthropic, etc.)
model: "gpt-4o-mini" # Model name
api_key: "" # Your API key (leave empty to use environment variable)
endpoint: "" # Custom endpoint URL (if using a proxy or alternative service)
max_tokens: 100 # Maximum tokens for response
temperature: 0.7 # Temperature for generation
# Image Processing
image:
supported_formats:
- ".jpg"
- ".jpeg"
- ".png"
- ".gif"
- ".webp"
# Prompt Configuration
prompt:
system_message: "You are a helpful assistant that generates concise, descriptive filenames for images. Focus on the main subject, key attributes, and context. Use snake_case format without special characters."
user_message: "Generate a descriptive filename for this image. The filename should be concise, specific, and use snake_case format. Do not include a file extension."

207
src/pynamer/core.py Normal file
View File

@ -0,0 +1,207 @@
"""Core functionality for PyNamer."""
import base64
import os
import sys
from pathlib import Path
import yaml
from typing import Dict, List, Optional, Union
import litellm
from litellm import completion
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('pynamer')
class PyNamer:
"""A tool to generate descriptive filenames for images using LLMs."""
def __init__(self, config_path: str = None):
"""Initialize the PyNamer with configuration.
Args:
config_path: Path to the YAML configuration file
"""
if config_path is None:
# Look for config in user's home directory first
user_config_path = os.path.expanduser("~/.config/pynamer.yaml")
if os.path.exists(user_config_path):
config_path = user_config_path
logger.info(f"Using user config from {user_config_path}")
else:
# Fall back to default config in package
config_path = os.path.join(os.path.dirname(__file__), 'config.yaml')
logger.info(f"Using default config from {config_path}")
self.config = self._load_config(config_path)
self._setup_llm()
def _load_config(self, config_path: str) -> Dict:
"""Load configuration from YAML file.
Args:
config_path: Path to the configuration file
Returns:
Dict containing configuration
"""
try:
with open(config_path, 'r') as f:
config = yaml.safe_load(f)
logger.info(f"Loaded configuration from {config_path}")
return config
except Exception as e:
logger.error(f"Failed to load configuration: {e}")
sys.exit(1)
def _setup_llm(self) -> None:
"""Set up the LLM client based on configuration."""
llm_config = self.config.get('llm', {})
# Set API key if provided in config
api_key = llm_config.get('api_key')
if api_key:
os.environ["OPENAI_API_KEY"] = api_key
# Set custom endpoint if provided
endpoint = llm_config.get('endpoint')
if endpoint:
os.environ["OPENAI_API_BASE"] = endpoint
self.model = llm_config.get('model', 'gpt-4-vision-preview')
self.max_tokens = llm_config.get('max_tokens', 100)
self.temperature = llm_config.get('temperature', 0.7)
logger.info(f"LLM setup complete. Using model: {self.model}")
def _encode_image(self, image_path: str) -> str:
"""Encode image to base64 for API submission.
Args:
image_path: Path to the image file
Returns:
Base64 encoded image string
"""
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
def _is_supported_format(self, file_path: str) -> bool:
"""Check if the file format is supported.
Args:
file_path: Path to the file
Returns:
True if supported, False otherwise
"""
supported_formats = self.config.get('image', {}).get('supported_formats', [])
file_ext = os.path.splitext(file_path)[1].lower()
return file_ext in supported_formats
def generate_filename(self, image_path: str) -> str:
"""Generate a descriptive filename for the image using LLM.
Args:
image_path: Path to the image file
Returns:
Generated filename (without extension)
"""
if not os.path.exists(image_path):
logger.error(f"Image not found: {image_path}")
return None
if not self._is_supported_format(image_path):
logger.error(f"Unsupported file format: {image_path}")
return None
try:
# Encode image
base64_image = self._encode_image(image_path)
# Prepare messages for LLM
system_message = self.config.get('prompt', {}).get('system_message', '')
user_message = self.config.get('prompt', {}).get('user_message', '')
messages = [
{"role": "system", "content": system_message},
{
"role": "user",
"content": [
{"type": "text", "text": user_message},
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}
}
]
}
]
# Call LLM
response = completion(
model=self.model,
messages=messages,
max_tokens=self.max_tokens,
temperature=self.temperature
)
# Extract filename from response
filename = response.choices[0].message.content.strip()
logger.info(f"Generated filename: {filename}")
return filename
except Exception as e:
logger.error(f"Error generating filename: {e}")
return None
def rename_image(self, image_path: str, dry_run: bool = False) -> Optional[str]:
"""Rename the image with a generated descriptive filename.
Args:
image_path: Path to the image file
dry_run: If True, don't actually rename the file
Returns:
New path if successful, None otherwise
"""
# Generate filename
new_filename = self.generate_filename(image_path)
if not new_filename:
return None
# Clean up the filename (ensure snake_case, no special chars)
new_filename = new_filename.lower()
new_filename = ''.join(c if c.isalnum() else '_' for c in new_filename)
new_filename = new_filename.replace('__', '_').strip('_')
# Get original path components
path = Path(image_path)
directory = path.parent
extension = path.suffix
# Create new path
new_path = directory / f"{new_filename}{extension}"
# Rename file
if not dry_run:
try:
# Handle case where the new filename already exists
counter = 1
while new_path.exists():
new_path = directory / f"{new_filename}_{counter}{extension}"
counter += 1
path.rename(new_path)
logger.info(f"Renamed: {image_path} -> {new_path}")
except Exception as e:
logger.error(f"Error renaming file: {e}")
return None
else:
logger.info(f"[DRY RUN] Would rename: {image_path} -> {new_path}")
return str(new_path)

View File

@ -0,0 +1,59 @@
"""Utilities for setting up PyNamer."""
import os
import shutil
import logging
import yaml
logger = logging.getLogger('pynamer')
def ensure_user_config_exists():
"""
Ensure that the user config file exists in ~/.config/pynamer.yaml.
If it doesn't exist, copy the default config there.
"""
# Get the default config path from the package
default_config_path = os.path.join(os.path.dirname(__file__), 'config.yaml')
# Define the user config path
user_config_dir = os.path.expanduser("~/.config")
user_config_path = os.path.join(user_config_dir, "pynamer.yaml")
# Check if the user config already exists
if os.path.exists(user_config_path):
logger.info(f"User config already exists at {user_config_path}")
return
# Create the ~/.config directory if it doesn't exist
if not os.path.exists(user_config_dir):
try:
os.makedirs(user_config_dir, exist_ok=True)
logger.info(f"Created directory: {user_config_dir}")
except Exception as e:
logger.error(f"Failed to create directory {user_config_dir}: {e}")
return
# Copy the default config to the user config path
try:
shutil.copy2(default_config_path, user_config_path)
logger.info(f"Copied default config to {user_config_path}")
except Exception as e:
logger.error(f"Failed to copy default config to {user_config_path}: {e}")
return
# Optionally, you can modify the user config here
# For example, to add a comment about it being a user-specific config
try:
with open(user_config_path, 'r') as f:
config = yaml.safe_load(f)
# Add a comment at the top of the file
with open(user_config_path, 'w') as f:
f.write("# PyNamer User Configuration\n")
f.write("# This file was automatically created during installation\n")
f.write("# You can modify this file to customize PyNamer's behavior\n\n")
yaml.dump(config, f, default_flow_style=False)
logger.info(f"Updated user config with comments")
except Exception as e:
logger.error(f"Failed to update user config with comments: {e}")