Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/immich-app/immich into feat…
Browse files Browse the repository at this point in the history
…/inline-offline-check
  • Loading branch information
etnoy committed Jan 15, 2025
2 parents fc05608 + e151248 commit 9d8a9d9
Show file tree
Hide file tree
Showing 33 changed files with 707 additions and 523 deletions.
38 changes: 20 additions & 18 deletions docs/docs/install/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,24 +148,26 @@ Redis (Sentinel) URL example JSON before encoding:

## Machine Learning

| Variable | Description | Default | Containers |
| :-------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning |
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO) | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning |
| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning |
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
| Variable | Description | Default | Containers |
| :---------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning |
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO) | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Name of the textual CLIP model to be preloaded and kept in cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Name of the visual CLIP model to be preloaded and kept in cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Name of the recognition portion of the facial recognition model to be preloaded and kept in cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Name of the detection portion of the facial recognition model to be preloaded and kept in cache | | machine learning |
| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning |
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |

\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.

Expand Down
4 changes: 2 additions & 2 deletions e2e/src/api/specs/oauth.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';

const authServer = {
internal: 'http://auth-server:3000',
external: 'http://127.0.0.1:3000',
internal: 'http://auth-server:2286',
external: 'http://127.0.0.1:2286',
};

const mobileOverrideRedirectUri = 'https://photos.immich.app/oauth/mobile-redirect';
Expand Down
2 changes: 1 addition & 1 deletion e2e/src/setup/auth-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const setup = async () => {
const { privateKey, publicKey } = await generateKeyPair('RS256');

const redirectUris = ['http://127.0.0.1:2285/auth/login', 'https://photos.immich.app/oauth/mobile-redirect'];
const port = 3000;
const port = 2286;
const host = '0.0.0.0';
const oidc = new Provider(`http://${host}:${port}`, {
renderError: async (ctx, out, error) => {
Expand Down
36 changes: 34 additions & 2 deletions machine-learning/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,41 @@
from uvicorn.workers import UvicornWorker


class ClipSettings(BaseModel):
textual: str | None = None
visual: str | None = None


class FacialRecognitionSettings(BaseModel):
recognition: str | None = None
detection: str | None = None


class PreloadModelData(BaseModel):
clip: str | None = None
facial_recognition: str | None = None
clip: ClipSettings = ClipSettings()
facial_recognition: FacialRecognitionSettings = FacialRecognitionSettings()

clip_model_fallback: str | None = os.getenv("MACHINE_LEARNING_PRELOAD__CLIP", None)
facial_recognition_model_fallback: str | None = os.getenv("MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION", None)

def update_from_fallbacks(self) -> None:
if self.clip_model_fallback:
self.clip.textual = self.clip_model_fallback
self.clip.visual = self.clip_model_fallback
log.warning(
"Deprecated env variable: MACHINE_LEARNING_PRELOAD__CLIP. "
"Use MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL and "
"MACHINE_LEARNING_PRELOAD__CLIP__VISUAL instead."
)

if self.facial_recognition_model_fallback:
self.facial_recognition.recognition = self.facial_recognition_model_fallback
self.facial_recognition.detection = self.facial_recognition_model_fallback
log.warning(
"Deprecated environment variable: MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION. "
"Use MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION and "
"MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION instead."
)


class MaxBatchSize(BaseModel):
Expand Down
23 changes: 17 additions & 6 deletions machine-learning/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,29 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:

async def preload_models(preload: PreloadModelData) -> None:
log.info(f"Preloading models: {preload}")
if preload.clip is not None:
model = await model_cache.get(preload.clip, ModelType.TEXTUAL, ModelTask.SEARCH)

if preload.clip.textual is not None:
model = await model_cache.get(preload.clip.textual, ModelType.TEXTUAL, ModelTask.SEARCH)
await load(model)

model = await model_cache.get(preload.clip, ModelType.VISUAL, ModelTask.SEARCH)
if preload.clip.visual is not None:
model = await model_cache.get(preload.clip.visual, ModelType.VISUAL, ModelTask.SEARCH)
await load(model)

if preload.facial_recognition is not None:
model = await model_cache.get(preload.facial_recognition, ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)
if preload.facial_recognition.detection is not None:
model = await model_cache.get(
preload.facial_recognition.detection,
ModelType.DETECTION,
ModelTask.FACIAL_RECOGNITION,
)
await load(model)

model = await model_cache.get(preload.facial_recognition, ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION)
if preload.facial_recognition.recognition is not None:
model = await model_cache.get(
preload.facial_recognition.recognition,
ModelType.RECOGNITION,
ModelTask.FACIAL_RECOGNITION,
)
await load(model)


Expand Down
24 changes: 16 additions & 8 deletions machine-learning/app/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,11 +700,13 @@ async def test_raises_exception_if_unknown_model_name(self) -> None:
await model_cache.get("test_model_name", ModelType.TEXTUAL, ModelTask.SEARCH)

async def test_preloads_clip_models(self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock) -> None:
os.environ["MACHINE_LEARNING_PRELOAD__CLIP"] = "ViT-B-32__openai"
os.environ["MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL"] = "ViT-B-32__openai"
os.environ["MACHINE_LEARNING_PRELOAD__CLIP__VISUAL"] = "ViT-B-32__openai"

settings = Settings()
assert settings.preload is not None
assert settings.preload.clip == "ViT-B-32__openai"
assert settings.preload.clip.textual == "ViT-B-32__openai"
assert settings.preload.clip.visual == "ViT-B-32__openai"

model_cache = ModelCache()
monkeypatch.setattr("app.main.model_cache", model_cache)
Expand All @@ -721,11 +723,13 @@ async def test_preloads_clip_models(self, monkeypatch: MonkeyPatch, mock_get_mod
async def test_preloads_facial_recognition_models(
self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock
) -> None:
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"] = "buffalo_s"
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION"] = "buffalo_s"
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION"] = "buffalo_s"

settings = Settings()
assert settings.preload is not None
assert settings.preload.facial_recognition == "buffalo_s"
assert settings.preload.facial_recognition.detection == "buffalo_s"
assert settings.preload.facial_recognition.recognition == "buffalo_s"

model_cache = ModelCache()
monkeypatch.setattr("app.main.model_cache", model_cache)
Expand All @@ -740,13 +744,17 @@ async def test_preloads_facial_recognition_models(
)

async def test_preloads_all_models(self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock) -> None:
os.environ["MACHINE_LEARNING_PRELOAD__CLIP"] = "ViT-B-32__openai"
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"] = "buffalo_s"
os.environ["MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL"] = "ViT-B-32__openai"
os.environ["MACHINE_LEARNING_PRELOAD__CLIP__VISUAL"] = "ViT-B-32__openai"
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION"] = "buffalo_s"
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION"] = "buffalo_s"

settings = Settings()
assert settings.preload is not None
assert settings.preload.clip == "ViT-B-32__openai"
assert settings.preload.facial_recognition == "buffalo_s"
assert settings.preload.clip.visual == "ViT-B-32__openai"
assert settings.preload.clip.textual == "ViT-B-32__openai"
assert settings.preload.facial_recognition.recognition == "buffalo_s"
assert settings.preload.facial_recognition.detection == "buffalo_s"

model_cache = ModelCache()
monkeypatch.setattr("app.main.model_cache", model_cache)
Expand Down
4 changes: 2 additions & 2 deletions server/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20250107@sha256:d00ab37e1c1ed87b799d6509fbc825a721ca0723c59c67955217826882017d38 AS dev
FROM ghcr.io/immich-app/base-server-dev:20250114@sha256:fce0404484bde5afc38a4399c6b25895eb079a666d269f199c93dfbfdd5b26b6 AS dev

RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
Expand Down Expand Up @@ -42,7 +42,7 @@ RUN npm run build


# prod build
FROM ghcr.io/immich-app/base-server-prod:20250107@sha256:78e92f113103271d43a3b050370b21b31c3c14792d3d23b18b542581a440c72b
FROM ghcr.io/immich-app/base-server-prod:20250114@sha256:94ec8a36cdf11691810c4aeccee1b49b00348e17f6b6781d87dd48a74e6c6787

WORKDIR /usr/src/app
ENV NODE_ENV=production \
Expand Down
1 change: 1 addition & 0 deletions server/src/bin/sync-sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class SqlGenerator {
this.sqlLogger.logQuery(event.query.sql);
} else if (event.level === 'error') {
this.sqlLogger.logQueryError(event.error as Error, event.query.sql);
this.sqlLogger.logQuery(event.query.sql);
}
},
}),
Expand Down
16 changes: 10 additions & 6 deletions server/src/interfaces/album-user.interface.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { AlbumUserEntity } from 'src/entities/album-user.entity';
import { Insertable, Selectable, Updateable } from 'kysely';
import { AlbumsSharedUsersUsers } from 'src/db';

export const IAlbumUserRepository = 'IAlbumUserRepository';

export type AlbumPermissionId = {
albumId: string;
userId: string;
albumsId: string;
usersId: string;
};

export interface IAlbumUserRepository {
create(albumUser: Partial<AlbumUserEntity>): Promise<AlbumUserEntity>;
update({ userId, albumId }: AlbumPermissionId, albumPermission: Partial<AlbumUserEntity>): Promise<AlbumUserEntity>;
delete({ userId, albumId }: AlbumPermissionId): Promise<void>;
create(albumUser: Insertable<AlbumsSharedUsersUsers>): Promise<Selectable<AlbumsSharedUsersUsers>>;
update(
id: AlbumPermissionId,
albumPermission: Updateable<AlbumsSharedUsersUsers>,
): Promise<Selectable<AlbumsSharedUsersUsers>>;
delete(id: AlbumPermissionId): Promise<void>;
}
8 changes: 5 additions & 3 deletions server/src/interfaces/library.interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Insertable, Updateable } from 'kysely';
import { Libraries } from 'src/db';
import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
import { LibraryEntity } from 'src/entities/library.entity';

Expand All @@ -13,10 +15,10 @@ export enum AssetSyncResult {
export interface ILibraryRepository {
getAll(withDeleted?: boolean): Promise<LibraryEntity[]>;
getAllDeleted(): Promise<LibraryEntity[]>;
get(id: string, withDeleted?: boolean): Promise<LibraryEntity | null>;
create(library: Partial<LibraryEntity>): Promise<LibraryEntity>;
get(id: string, withDeleted?: boolean): Promise<LibraryEntity | undefined>;
create(library: Insertable<Libraries>): Promise<LibraryEntity>;
delete(id: string): Promise<void>;
softDelete(id: string): Promise<void>;
update(library: Partial<LibraryEntity>): Promise<LibraryEntity>;
update(id: string, library: Updateable<Libraries>): Promise<LibraryEntity>;
getStatistics(id: string): Promise<LibraryStatsResponseDto | undefined>;
}
11 changes: 8 additions & 3 deletions server/src/interfaces/memory.interface.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { MemoryEntity } from 'src/entities/memory.entity';
import { Insertable, Updateable } from 'kysely';
import { Memories } from 'src/db';
import { MemoryEntity, OnThisDayData } from 'src/entities/memory.entity';
import { IBulkAsset } from 'src/utils/asset.util';

export const IMemoryRepository = 'IMemoryRepository';

export interface IMemoryRepository extends IBulkAsset {
search(ownerId: string): Promise<MemoryEntity[]>;
get(id: string): Promise<MemoryEntity | null>;
create(memory: Partial<MemoryEntity>): Promise<MemoryEntity>;
update(memory: Partial<MemoryEntity>): Promise<MemoryEntity>;
create(
memory: Omit<Insertable<Memories>, 'data'> & { data: OnThisDayData },
assetIds: Set<string>,
): Promise<MemoryEntity>;
update(id: string, memory: Updateable<Memories>): Promise<MemoryEntity>;
delete(id: string): Promise<void>;
}
25 changes: 25 additions & 0 deletions server/src/queries/album.user.repository.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- NOTE: This file is auto generated by ./sql-generator

-- AlbumUserRepository.create
insert into
"albums_shared_users_users" ("usersId", "albumsId")
values
($1, $2)
returning
*

-- AlbumUserRepository.update
update "albums_shared_users_users"
set
"role" = $1
where
"usersId" = $2
and "albumsId" = $3
returning
*

-- AlbumUserRepository.delete
delete from "albums_shared_users_users"
where
"usersId" = $1
and "albumsId" = $2
Loading

0 comments on commit 9d8a9d9

Please sign in to comment.