automated update: 2026-06-11 00:34:20
This commit is contained in:
@@ -0,0 +1,53 @@
|
|||||||
|
# .github/workflows/update-lockfile.yml
|
||||||
|
name: Update uv.lock
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'pyproject.toml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-lockfile:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Free up disk space
|
||||||
|
run: |
|
||||||
|
sudo rm -rf /usr/share/dotnet
|
||||||
|
sudo rm -rf /opt/ghc
|
||||||
|
sudo rm -rf "/usr/local/share/boost"
|
||||||
|
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||||
|
echo "Disk space freed."
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
run: uv python install 3.12
|
||||||
|
|
||||||
|
- name: Regenerate lockfile
|
||||||
|
run: uv lock
|
||||||
|
|
||||||
|
- name: Check for changes
|
||||||
|
id: diff
|
||||||
|
run: |
|
||||||
|
if git diff --quiet uv.lock; then
|
||||||
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit and push updated lockfile
|
||||||
|
if: steps.diff.outputs.changed == 'true'
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add uv.lock
|
||||||
|
git commit -m "chore: update uv.lock"
|
||||||
|
git push
|
||||||
|
|
||||||
@@ -44,6 +44,10 @@ RUN chmod +x /app/entrypoint.sh
|
|||||||
# ── Runtime stage ─────────────────────────────────────────────────────────
|
# ── Runtime stage ─────────────────────────────────────────────────────────
|
||||||
FROM build AS runtime
|
FROM build AS runtime
|
||||||
|
|
||||||
|
# Expose cuDNN libraries installed by nvidia-cudnn-cu12 pip package
|
||||||
|
# so onnxruntime-gpu can find libcudnn.so.9 at runtime
|
||||||
|
ENV LD_LIBRARY_PATH="/app/.venv/lib/python3.12/site-packages/nvidia/cudnn/lib:${LD_LIBRARY_PATH}"
|
||||||
|
|
||||||
RUN groupadd -g 568 apps && useradd -u 568 -g apps -m -s /bin/bash appuser \
|
RUN groupadd -g 568 apps && useradd -u 568 -g apps -m -s /bin/bash appuser \
|
||||||
&& mkdir -p /models/.insightface /models/huggingface \
|
&& mkdir -p /models/.insightface /models/huggingface \
|
||||||
&& chown -R appuser:apps /app /models
|
&& chown -R appuser:apps /app /models
|
||||||
|
|||||||
+84
-2
@@ -16,7 +16,7 @@ from .config import Config, ConfigManager
|
|||||||
from .diversity import select_diverse_assets
|
from .diversity import select_diverse_assets
|
||||||
from .embeddings import is_embedding_available, load_embedding_model
|
from .embeddings import is_embedding_available, load_embedding_model
|
||||||
from .image_processing import process_face_mode, process_full_mode, process_object_mode
|
from .image_processing import process_face_mode, process_full_mode, process_object_mode
|
||||||
from .immich_api import fetch_all_assets, fetch_full_image, filter_recent_assets, get_people
|
from .immich_api import fetch_all_assets, fetch_face_data, fetch_full_image, filter_recent_assets, get_people
|
||||||
from .logging import console, setup_logging
|
from .logging import console, setup_logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -422,6 +422,51 @@ def _show_preview(jobs: list[dict]) -> None:
|
|||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def _enrich_asset_with_face_data(asset: dict, person: dict) -> dict:
|
||||||
|
"""Enrich an asset dict with face data from the Immich faces API.
|
||||||
|
|
||||||
|
The search/metadata endpoint does not include face bounding box data,
|
||||||
|
so we fetch it from GET /api/faces?id={asset_id} and inject it into
|
||||||
|
the asset's "people" field so process_face_mode can find it.
|
||||||
|
|
||||||
|
Returns the enriched asset dict (modifies in place and returns it).
|
||||||
|
"""
|
||||||
|
person_id = person["id"]
|
||||||
|
face_data = fetch_face_data(asset["id"], person_id=person_id)
|
||||||
|
|
||||||
|
if face_data is None:
|
||||||
|
logger.debug(
|
||||||
|
f"Face data API returned nothing for {person.get('name')} "
|
||||||
|
f"in asset {asset.get('id')} — Immich may not have detected a face"
|
||||||
|
)
|
||||||
|
return asset
|
||||||
|
|
||||||
|
# Skip zero-area bounding boxes (face detection failed or no face found)
|
||||||
|
if face_data.bbox == (0, 0, 0, 0):
|
||||||
|
logger.debug(
|
||||||
|
f"Zero-area bounding box for {person.get('name')} in asset {asset.get('id')}"
|
||||||
|
)
|
||||||
|
return asset
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Got face data for {person.get('name')} in asset {asset.get('id')}: "
|
||||||
|
f"bbox={face_data.bbox}, img_size={face_data.image_width}x{face_data.image_height}"
|
||||||
|
)
|
||||||
|
|
||||||
|
face_info = {
|
||||||
|
"boundingBoxX1": face_data.bbox[0],
|
||||||
|
"boundingBoxY1": face_data.bbox[1],
|
||||||
|
"boundingBoxX2": face_data.bbox[2],
|
||||||
|
"boundingBoxY2": face_data.bbox[3],
|
||||||
|
"imageWidth": face_data.image_width,
|
||||||
|
"imageHeight": face_data.image_height,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Inject into asset so process_face_mode can find it via asset["people"]
|
||||||
|
asset["people"] = [{"id": person_id, "faces": [face_info]}]
|
||||||
|
return asset
|
||||||
|
|
||||||
|
|
||||||
def execute_jobs(jobs: list[dict]) -> None:
|
def execute_jobs(jobs: list[dict]) -> None:
|
||||||
"""Download and process images for all jobs."""
|
"""Download and process images for all jobs."""
|
||||||
if not jobs:
|
if not jobs:
|
||||||
@@ -450,8 +495,17 @@ def execute_jobs(jobs: list[dict]) -> None:
|
|||||||
os.makedirs(person_dir, exist_ok=True)
|
os.makedirs(person_dir, exist_ok=True)
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
|
skipped_download = 0
|
||||||
|
skipped_no_face = 0
|
||||||
|
skipped_other = 0
|
||||||
|
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
try:
|
try:
|
||||||
|
# For face mode, enrich the asset with face bounding box data
|
||||||
|
# from the Immich faces API (not included in search/metadata results)
|
||||||
|
if mode == "face":
|
||||||
|
asset = _enrich_asset_with_face_data(asset, person)
|
||||||
|
|
||||||
# Use full-resolution for final output when configured
|
# Use full-resolution for final output when configured
|
||||||
if use_full_res:
|
if use_full_res:
|
||||||
img = fetch_full_image(asset["id"])
|
img = fetch_full_image(asset["id"])
|
||||||
@@ -464,7 +518,9 @@ def execute_jobs(jobs: list[dict]) -> None:
|
|||||||
img = Image.open(BytesIO(resp.content)) if resp.ok else None
|
img = Image.open(BytesIO(resp.content)) if resp.ok else None
|
||||||
|
|
||||||
if img is None:
|
if img is None:
|
||||||
progress.console.print(f"[red]Failed download {asset['id']}[/red]")
|
skipped_download += 1
|
||||||
|
progress.console.print(f"[red]✗ Failed download {asset['id']}[/red]")
|
||||||
|
logger.debug(f"Image download failed for asset {asset['id']}")
|
||||||
else:
|
else:
|
||||||
saved = (
|
saved = (
|
||||||
process_face_mode(img, asset, person, person_dir, count)
|
process_face_mode(img, asset, person, person_dir, count)
|
||||||
@@ -475,6 +531,22 @@ def execute_jobs(jobs: list[dict]) -> None:
|
|||||||
)
|
)
|
||||||
if saved:
|
if saved:
|
||||||
count += 1
|
count += 1
|
||||||
|
logger.debug(f"Saved image #{count} from asset {asset['id']}")
|
||||||
|
else:
|
||||||
|
if mode == "face":
|
||||||
|
skipped_no_face += 1
|
||||||
|
progress.console.print(
|
||||||
|
f"[yellow]⏭ No usable face data for {asset['id']}[/yellow]"
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"process_face_mode returned False for asset {asset['id']} — "
|
||||||
|
f"people={asset.get('people', 'MISSING')}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
skipped_other += 1
|
||||||
|
progress.console.print(
|
||||||
|
f"[yellow]⏭ Skipped {asset['id']}[/yellow]"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to process asset {asset['id']}: {e}")
|
logger.error(f"Failed to process asset {asset['id']}: {e}")
|
||||||
|
|
||||||
@@ -483,6 +555,16 @@ def execute_jobs(jobs: list[dict]) -> None:
|
|||||||
|
|
||||||
progress.remove_task(job_task)
|
progress.remove_task(job_task)
|
||||||
|
|
||||||
|
# Per-person execution summary
|
||||||
|
rprint(
|
||||||
|
f"\n [bold]{name}:[/bold] saved {count}/{len(assets)} images "
|
||||||
|
f"(download_failed={skipped_download}, no_face_data={skipped_no_face}, other={skipped_other})"
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"{name}: saved {count}/{len(assets)} "
|
||||||
|
f"(download_failed={skipped_download}, no_face_data={skipped_no_face}, other={skipped_other})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Entry point for if-curator CLI."""
|
"""Entry point for if-curator CLI."""
|
||||||
|
|||||||
+3
-1
@@ -16,6 +16,7 @@ classifiers = [
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"croniter>=5.0.2",
|
"croniter>=5.0.2",
|
||||||
"insightface>=0.7.3",
|
"insightface>=0.7.3",
|
||||||
|
"nvidia-cudnn-cu12>=9.0.0",
|
||||||
"numpy>=2.2.6",
|
"numpy>=2.2.6",
|
||||||
"onnxruntime-gpu>=1.23.2",
|
"onnxruntime-gpu>=1.23.2",
|
||||||
"opencv-python-headless>=4.12.0.88",
|
"opencv-python-headless>=4.12.0.88",
|
||||||
@@ -76,6 +77,7 @@ opencv-python-headless = "cv2"
|
|||||||
python-dotenv = "dotenv"
|
python-dotenv = "dotenv"
|
||||||
insightface = "insightface"
|
insightface = "insightface"
|
||||||
numpy = "numpy"
|
numpy = "numpy"
|
||||||
|
nvidia-cudnn-cu12 = "nvidia.cudnn"
|
||||||
onnxruntime-gpu = "onnxruntime"
|
onnxruntime-gpu = "onnxruntime"
|
||||||
requests = "requests"
|
requests = "requests"
|
||||||
rich = "rich"
|
rich = "rich"
|
||||||
@@ -84,7 +86,7 @@ transformers = "transformers"
|
|||||||
ultralytics = "ultralytics"
|
ultralytics = "ultralytics"
|
||||||
|
|
||||||
[tool.deptry.per_rule_ignores]
|
[tool.deptry.per_rule_ignores]
|
||||||
DEP002 = ["onnxruntime-gpu"]
|
DEP002 = ["onnxruntime-gpu", "nvidia-cudnn-cu12"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["hatchling"]
|
||||||
|
|||||||
Reference in New Issue
Block a user