From 3b93c5988b89967cdec767773b5de51d45a9913d Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 2 Oct 2026 20:56:02 +0200 Subject: [PATCH] quality bot --- src/photools3/cli.py | 27 +++++++++++++++- src/photools3/llm_session.py | 60 +++++++++++++++++++++++++++++------- src/photools3/models.py | 18 +++++++++-- 3 files changed, 90 insertions(+), 15 deletions(-) diff --git a/src/photools3/cli.py b/src/photools3/cli.py index 2071050..a5d2020 100644 --- a/src/photools3/cli.py +++ b/src/photools3/cli.py @@ -3,7 +3,7 @@ from typing import Annotated, Any import typer -from photools3.llm_session import LlmSessionCaptionsAndKeywords +from photools3.llm_session import LlmSessionCaptionsAndKeywords, LlmSessionQuality from photools3.photo import Photo app = typer.Typer(help="PhotoTools3: A CLI for image processing.") @@ -44,12 +44,37 @@ def keywords( raise typer.Exit(code=1) +@app.command("quality") +def quality( + file_path: Annotated[Path, typer.Argument(..., help="Path to the image file")], +): + """Assess general quality of photos""" + 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_qa(file_path) + # _print_results(result) + print(result) + except Exception as e: # noqa: BLE001 + 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 _run_qa(file_path: Path) -> Any: + """Handles the core logic of loading the photo and calling the LLM.""" + session = LlmSessionQuality() + photo = Photo(file_path) + return session.call([photo]) + def _print_results(result: Any) -> None: """Formats and prints the extraction results to the console.""" diff --git a/src/photools3/llm_session.py b/src/photools3/llm_session.py index 492cfee..3dde285 100644 --- a/src/photools3/llm_session.py +++ b/src/photools3/llm_session.py @@ -64,17 +64,18 @@ class LlmSession: request = self._build_request(prompt, photos, model_id) result_raw = self.client_chat_completion_create(request) - try: - result = json.loads(result_raw) - except json.JSONDecodeError as e: - try: - json_str = re.sub( - r"^```json(.*)```$", r"\1", result_raw, flags=re.DOTALL - ).strip() - result = json.loads(json_str) - except json.JSONDecodeError: - raise e - return result + # try: + # result = json.loads(result_raw) + # except json.JSONDecodeError as e: + # try: + # json_str = re.sub( + # r"^```json(.*)```$", r"\1", result_raw, flags=re.DOTALL + # ).strip() + # result = json.loads(json_str) + # except json.JSONDecodeError: + # raise e + # return result + return result_raw def _build_request( self, @@ -144,3 +145,40 @@ class LlmSessionCaptionsAndKeywords(LlmSession): photos=photos, model_id=model_id, ) + + +class LlmSessionQuality(LlmSession): + + def __init__(self, temperature: float = settings.llm_default_temperature): + super().__init__( + system_prompt=( + "You are a helpful photographer's assistant that helps to identify " + "photos that can be sorted out due to qulity issues:\n" + "- Your answer is a simple string that must strictly comply with this scheme: " + "'{flag}|{reason}', " + "where {flag} and {reason} are placeholders for possible values " + "that are explained in the following points.\n" + "- {flag} is the general image quality assessment." + "It can have the following values:\n" + " - `not_ok`: photo lacks basic quality and should almost certainly be deleted.\n" + " - `review`: photo has some quality issues, not severe but might be deleted.\n" + " - `ok`: no issues, photo can be kept.\n" + "- {reason} is the reason for the photo being flagged as `not_ok` or `review`. " + "Thus, for any photo that is not `ok`, one or more reasons have to be given. " + "If there is more than one reason, give a comma-separated list: " + "'{flag}|{reason1},{flag}|{reason2},...'. " + "Possible reasons for a lower-than-`ok` assessment are:\n" + " - `blur`: Obviously mis-focussed or motion-blurred.\n" + " - `exposure`: severly under- or over-exposed.\n" + " - `style`: photo has severe style issues (beyond paersonal taste)\n" + ), + temperature=temperature + ) + + def call(self, photos: list[Photo], model_id=None): + return super().call( + prompt="Assess the quality of the following photos to help me decide which ones to sort out", + photos=photos, + model_id=model_id, + ) + diff --git a/src/photools3/models.py b/src/photools3/models.py index 260c114..3df8e8d 100644 --- a/src/photools3/models.py +++ b/src/photools3/models.py @@ -1,7 +1,7 @@ -from enum import StrEnum -from typing import Annotated, Any +from enum import Enum, StrEnum +from typing import Annotated, Any, Literal -from pydantic import BaseModel, BeforeValidator, model_validator +from pydantic import BaseModel, BeforeValidator, Field, model_validator class LlmRequest(BaseModel): @@ -46,3 +46,15 @@ class LlmMessageContent(BaseModel): if self.type == "image_url" and self.image_url is None: raise ValueError("image_url must be set when type is 'image_url'") return self + +class LlmResponseKeywords(BaseModel): + caption: str = Field(description="Description of the image") + tags:list[str] = Field(description="keywords to tag the image") + +class Rating(Enum): + ok = Annotated[str, Field( description="bla" )] + review = Annotated[str, Field(description="")] + +class LlmResponseQuality(BaseModel): + rating = Literal["ok", "review", "not_ok"] + reason:str