diff --git a/pyproject.toml b/pyproject.toml index adf2336..e004ca3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "pydantic-settings>=2.15.0", "pytest>=9.1.1", "ruff>=0.16.9", + "typer>=0.12.0", "utils>=0.1.2", ] diff --git a/src/photools3/__init__.py b/src/photools3/__init__.py index d44fcbd..2ca297a 100644 --- a/src/photools3/__init__.py +++ b/src/photools3/__init__.py @@ -1,2 +1,4 @@ -def main() -> None: - print("Hello from photools3!") +from .cli import app + +def main(): + app() diff --git a/src/photools3/__main__.py b/src/photools3/__main__.py new file mode 100644 index 0000000..d5cf0fd --- /dev/null +++ b/src/photools3/__main__.py @@ -0,0 +1,4 @@ +from .cli import app + +if __name__ == "__main__": + app() diff --git a/src/photools3/cli.py b/src/photools3/cli.py new file mode 100644 index 0000000..023c9e3 --- /dev/null +++ b/src/photools3/cli.py @@ -0,0 +1,61 @@ +from pathlib import Path +from typing import Any, Optional + +import typer + +from photools3.llm_session import LlmSessionCaptionsAndKeywords +from photools3.photo import Photo + +app = typer.Typer(help="PhotoTools3: A CLI for image processing.") + +@app.callback(invoke_without_command=True) +def main( + ctx: typer.Context, + version: Optional[bool] = typer.Option(None, "--version", is_eager=True), +): + """ + Main entry point for PhotoTools3 CLI. + """ + if version: + typer.echo("photools3 version 0.1.0") + raise typer.Exit() + + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + raise typer.Exit() + +@app.command("keywords") +def keywords(file_path: Path = typer.Argument(..., help="Path to the image file")): + """Extract keywords from a single image.""" + if not file_path.exists(): + typer.echo(f"Error: File '{file_path}' does not exist.", err=True) + raise typer.Exit(code=1) + + try: + typer.echo(f"Processing: {file_path}") + result = _run_keywords_extraction(file_path) + _print_results(result) + except Exception as e: + typer.echo(f"Error processing image: {e}", err=True) + raise typer.Exit(code=1) + +def _run_keywords_extraction(file_path: Path) -> Any: + """Handles the core logic of loading the photo and calling the LLM.""" + session = LlmSessionCaptionsAndKeywords() + photo = Photo(file_path) + return session.call([photo]) + +def _print_results(result: Any) -> None: + """Formats and prints the extraction results to the console.""" + typer.echo("\n--- Results ---") + if isinstance(result, dict): + caption = result.get("caption", "No caption found.") + tags = result.get("tags", []) + typer.echo(f"Caption: {caption}") + typer.echo(f"Tags: {', '.join(tags)}") + else: + typer.echo(str(result)) + typer.echo("---------------") + +if __name__ == "__main__": + app() diff --git a/src/photools3/llm_session.py b/src/photools3/llm_session.py index 2cb55e1..492cfee 100644 --- a/src/photools3/llm_session.py +++ b/src/photools3/llm_session.py @@ -129,6 +129,7 @@ class LlmSessionCaptionsAndKeywords(LlmSession): " - Do not add any other text outside of this JSON format!\n" "- When writing captions, ONLY describe what is visible on the photo. Do not interpret!\n" "- Only use keywords from the keyword list, appended below.\n" + "- Answer in German.\n" "\n" "## Keyword list" "\n" diff --git a/uv.lock b/uv.lock index 983647f..4f2abe4 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.14" +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + [[package]] name = "annotated-types" version = "0.8.0" @@ -573,6 +582,7 @@ dependencies = [ { name = "pydantic-settings" }, { name = "pytest" }, { name = "ruff" }, + { name = "typer" }, { name = "utils" }, ] @@ -585,6 +595,7 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2.15.0" }, { name = "pytest", specifier = ">=9.1.1" }, { name = "ruff", specifier = ">=0.16.9" }, + { name = "typer", specifier = ">=0.12.0" }, { name = "utils", specifier = ">=0.1.2", index = "https://gitea.schneiderserver.site/api/packages/mathias2784/pypi/simple" }, ] @@ -898,6 +909,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -945,6 +965,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl", hash = "sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7", size = 43204, upload-time = "2026-07-27T15:58:59.26Z" }, ] +[[package]] +name = "typer" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0"