Llm client quality #14
+26
-1
@@ -3,7 +3,7 @@ from typing import Annotated, Any
|
|||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|
||||||
from photools3.llm_session import LlmSessionCaptionsAndKeywords
|
from photools3.llm_session import LlmSessionCaptionsAndKeywords, LlmSessionQuality
|
||||||
from photools3.photo import Photo
|
from photools3.photo import Photo
|
||||||
|
|
||||||
app = typer.Typer(help="PhotoTools3: A CLI for image processing.")
|
app = typer.Typer(help="PhotoTools3: A CLI for image processing.")
|
||||||
@@ -44,12 +44,37 @@ def keywords(
|
|||||||
raise typer.Exit(code=1)
|
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:
|
def _run_keywords_extraction(file_path: Path) -> Any:
|
||||||
"""Handles the core logic of loading the photo and calling the LLM."""
|
"""Handles the core logic of loading the photo and calling the LLM."""
|
||||||
session = LlmSessionCaptionsAndKeywords()
|
session = LlmSessionCaptionsAndKeywords()
|
||||||
photo = Photo(file_path)
|
photo = Photo(file_path)
|
||||||
return session.call([photo])
|
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:
|
def _print_results(result: Any) -> None:
|
||||||
"""Formats and prints the extraction results to the console."""
|
"""Formats and prints the extraction results to the console."""
|
||||||
|
|||||||
@@ -64,17 +64,18 @@ class LlmSession:
|
|||||||
request = self._build_request(prompt, photos, model_id)
|
request = self._build_request(prompt, photos, model_id)
|
||||||
result_raw = self.client_chat_completion_create(request)
|
result_raw = self.client_chat_completion_create(request)
|
||||||
|
|
||||||
try:
|
# try:
|
||||||
result = json.loads(result_raw)
|
# result = json.loads(result_raw)
|
||||||
except json.JSONDecodeError as e:
|
# except json.JSONDecodeError as e:
|
||||||
try:
|
# try:
|
||||||
json_str = re.sub(
|
# json_str = re.sub(
|
||||||
r"^```json(.*)```$", r"\1", result_raw, flags=re.DOTALL
|
# r"^```json(.*)```$", r"\1", result_raw, flags=re.DOTALL
|
||||||
).strip()
|
# ).strip()
|
||||||
result = json.loads(json_str)
|
# result = json.loads(json_str)
|
||||||
except json.JSONDecodeError:
|
# except json.JSONDecodeError:
|
||||||
raise e
|
# raise e
|
||||||
return result
|
# return result
|
||||||
|
return result_raw
|
||||||
|
|
||||||
def _build_request(
|
def _build_request(
|
||||||
self,
|
self,
|
||||||
@@ -144,3 +145,40 @@ class LlmSessionCaptionsAndKeywords(LlmSession):
|
|||||||
photos=photos,
|
photos=photos,
|
||||||
model_id=model_id,
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
+15
-3
@@ -1,7 +1,7 @@
|
|||||||
from enum import StrEnum
|
from enum import Enum, StrEnum
|
||||||
from typing import Annotated, Any
|
from typing import Annotated, Any, Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, BeforeValidator, model_validator
|
from pydantic import BaseModel, BeforeValidator, Field, model_validator
|
||||||
|
|
||||||
|
|
||||||
class LlmRequest(BaseModel):
|
class LlmRequest(BaseModel):
|
||||||
@@ -46,3 +46,15 @@ class LlmMessageContent(BaseModel):
|
|||||||
if self.type == "image_url" and self.image_url is None:
|
if self.type == "image_url" and self.image_url is None:
|
||||||
raise ValueError("image_url must be set when type is 'image_url'")
|
raise ValueError("image_url must be set when type is 'image_url'")
|
||||||
return self
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user