From 3e79c8cf28ed58a6c54712ead3638a6270d9e32b Mon Sep 17 00:00:00 2001
From: laurenspriem
Date: Thu, 23 May 2024 18:12:41 +0530
Subject: [PATCH 01/32] [mob][photos] Decrypt remote embeddings in computer
---
.../file_ml/remote_fileml_service.dart | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart b/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart
index eafbc6323d..bb1c9f999b 100644
--- a/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart
+++ b/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart
@@ -1,6 +1,7 @@
import "dart:async";
import "dart:convert";
+import "package:computer/computer.dart";
import "package:logging/logging.dart";
import "package:photos/core/network/network.dart";
import "package:photos/db/files_db.dart";
@@ -16,6 +17,8 @@ import "package:shared_preferences/shared_preferences.dart";
class RemoteFileMLService {
RemoteFileMLService._privateConstructor();
+ static final Computer _computer = Computer.shared();
+
static final RemoteFileMLService instance =
RemoteFileMLService._privateConstructor();
@@ -107,15 +110,17 @@ class RemoteFileMLService {
final input = EmbeddingsDecoderInput(embedding, fileKey);
inputs.add(input);
}
- // todo: use compute or isolate
- return decryptFileMLComputer(
- {
+ return _computer.compute
= ({ code, otp, nextOTP }) => {
color: "grey",
}}
>
- {code.account}
+ {code.account ?? ""}
{
return {
id,
type: parseType(url),
- account: _getAccount(uriPath),
+ account: parseAccount(url),
issuer: _getIssuer(uriPath, uriParams),
digits: parseDigits(url),
period: parsePeriod(url),
@@ -68,18 +68,9 @@ const parseType = (url: URL): Code["type"] => {
throw new Error(`Unsupported code with host ${t}`);
};
-const _getAccount = (uriPath: string): string => {
- try {
- const path = decodeURIComponent(uriPath);
- if (path.includes(":")) {
- return path.split(":")[1];
- } else if (path.includes("/")) {
- return path.split("/")[1];
- }
- } catch (e) {
- return "";
- }
-};
+/** Convert the pathname from "/ACME:user@example.org" => "user@example.org" */
+const parseAccount = (url: URL): string =>
+ url.pathname.split(":").at(-1).split("/").at(-1);
const _getIssuer = (uriPath: string, uriParams: { get?: any }): string => {
try {
diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json
index 1541878c51..0ec924b29b 100644
--- a/web/apps/photos/package.json
+++ b/web/apps/photos/package.json
@@ -43,7 +43,6 @@
"similarity-transformation": "^0.0.1",
"transformation-matrix": "^2.16",
"uuid": "^9.0.1",
- "vscode-uri": "^3.0.7",
"xml-js": "^1.6.11",
"zxcvbn": "^4.4.2"
},
diff --git a/web/yarn.lock b/web/yarn.lock
index 894a44dd02..aaa0d517a8 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -4804,11 +4804,6 @@ void-elements@3.1.0:
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
-vscode-uri@^3.0.7:
- version "3.0.8"
- resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f"
- integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
-
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
From bfe8fd83acfb7974cf39f827afd2ebee8d9eb255 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 09:29:54 +0530
Subject: [PATCH 05/32] Take 2
---
web/apps/auth/src/services/code.ts | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts
index ff29c58fdd..0acc537f0c 100644
--- a/web/apps/auth/src/services/code.ts
+++ b/web/apps/auth/src/services/code.ts
@@ -69,8 +69,12 @@ const parseType = (url: URL): Code["type"] => {
};
/** Convert the pathname from "/ACME:user@example.org" => "user@example.org" */
-const parseAccount = (url: URL): string =>
- url.pathname.split(":").at(-1).split("/").at(-1);
+const parseAccount = (url: URL): string => {
+ let p = url.pathname;
+ if (p.startsWith("/")) p = p.slice(1);
+ if (p.includes(":")) p = p.split(":").slice(1).join(":");
+ return p;
+};
const _getIssuer = (uriPath: string, uriParams: { get?: any }): string => {
try {
From 623b71715d08e8428a19edec97b1304981712e4e Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 09:42:23 +0530
Subject: [PATCH 06/32] Wrap
---
web/apps/auth/src/services/code.ts | 45 ++++++++++++++----------------
1 file changed, 21 insertions(+), 24 deletions(-)
diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts
index 0acc537f0c..4eb964d31d 100644
--- a/web/apps/auth/src/services/code.ts
+++ b/web/apps/auth/src/services/code.ts
@@ -53,7 +53,7 @@ export const codeFromURIString = (id: string, uriString: string): Code => {
id,
type: parseType(url),
account: parseAccount(url),
- issuer: _getIssuer(uriPath, uriParams),
+ issuer: parseIssuer(url),
digits: parseDigits(url),
period: parsePeriod(url),
secret: parseSecret(url),
@@ -68,37 +68,34 @@ const parseType = (url: URL): Code["type"] => {
throw new Error(`Unsupported code with host ${t}`);
};
-/** Convert the pathname from "/ACME:user@example.org" => "user@example.org" */
-const parseAccount = (url: URL): string => {
+const parseAccount = (url: URL): string | undefined => {
+ // "/ACME:user@example.org" => "user@example.org"
let p = url.pathname;
if (p.startsWith("/")) p = p.slice(1);
if (p.includes(":")) p = p.split(":").slice(1).join(":");
return p;
};
-const _getIssuer = (uriPath: string, uriParams: { get?: any }): string => {
- try {
- if (uriParams["issuer"] !== undefined) {
- let issuer = uriParams["issuer"];
- // This is to handle bug in the ente auth app
- if (issuer.endsWith("period")) {
- issuer = issuer.substring(0, issuer.length - 6);
- }
- return issuer;
+const parseIssuer = (url: URL): string => {
+ // If there is a "issuer" search param, use that.
+ let issuer = url.searchParams.get("issuer");
+ if (issuer !== undefined) {
+ // This is to handle bug in old versions of Ente Auth app.
+ if (issuer.endsWith("period")) {
+ issuer = issuer.substring(0, issuer.length - 6);
}
- let path = decodeURIComponent(uriPath);
- if (path.startsWith("totp/") || path.startsWith("hotp/")) {
- path = path.substring(5);
- }
- if (path.includes(":")) {
- return path.split(":")[0];
- } else if (path.includes("-")) {
- return path.split("-")[0];
- }
- return path;
- } catch (e) {
- return "";
+ return issuer;
}
+
+ // Otherwise use the `prefix:` from the account as the issuer.
+ // "/ACME:user@example.org" => "ACME"
+ let p = url.pathname;
+ if (p.startsWith("/")) p = p.slice(1);
+
+ if (p.includes(":")) p = p.split(":")[0];
+ else if (p.includes("-")) p = p.split("-")[0];
+
+ return p;
};
const parseDigits = (url: URL): number =>
From 59ed89cba172fdb668502d4d79345710e1e67225 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 09:49:20 +0530
Subject: [PATCH 07/32] .get returns null when the property is not present
---
web/apps/auth/src/services/code.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts
index 4eb964d31d..835bb689dd 100644
--- a/web/apps/auth/src/services/code.ts
+++ b/web/apps/auth/src/services/code.ts
@@ -79,7 +79,7 @@ const parseAccount = (url: URL): string | undefined => {
const parseIssuer = (url: URL): string => {
// If there is a "issuer" search param, use that.
let issuer = url.searchParams.get("issuer");
- if (issuer !== undefined) {
+ if (issuer) {
// This is to handle bug in old versions of Ente Auth app.
if (issuer.endsWith("period")) {
issuer = issuer.substring(0, issuer.length - 6);
From 4fa59ce2589de9b7c3df11734ec647039e8f1d0f Mon Sep 17 00:00:00 2001
From: laurenspriem
Date: Fri, 24 May 2024 09:56:10 +0530
Subject: [PATCH 08/32] [mob][photos] Common ml util for getting indexable
files across faces and clip
---
.../services/machine_learning/face_ml/face_ml_service.dart | 7 +------
.../semantic_search/semantic_search_service.dart | 7 +++----
mobile/lib/ui/settings/machine_learning_settings_page.dart | 3 ++-
mobile/lib/utils/ml_util.dart | 7 +++++++
4 files changed, 13 insertions(+), 11 deletions(-)
create mode 100644 mobile/lib/utils/ml_util.dart
diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
index 38079753c2..9bfa54f1ea 100644
--- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
+++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
@@ -12,7 +12,6 @@ import "package:flutter/foundation.dart" show debugPrint, kDebugMode;
import "package:logging/logging.dart";
import "package:onnxruntime/onnxruntime.dart";
import "package:package_info_plus/package_info_plus.dart";
-import "package:photos/core/configuration.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/db/files_db.dart";
import "package:photos/events/diff_sync_complete_event.dart";
@@ -48,6 +47,7 @@ import "package:photos/utils/file_util.dart";
import 'package:photos/utils/image_ml_isolate.dart';
import "package:photos/utils/image_ml_util.dart";
import "package:photos/utils/local_settings.dart";
+import "package:photos/utils/ml_util.dart";
import "package:photos/utils/network_util.dart";
import "package:photos/utils/thumbnail_util.dart";
import "package:synchronized/synchronized.dart";
@@ -1184,11 +1184,6 @@ class FaceMlService {
return ratio;
}
- static Future> getIndexableFileIDs() async {
- return FilesDB.instance
- .getOwnedFileIDs(Configuration.instance.getUserID()!);
- }
-
bool _skipAnalysisEnteFile(EnteFile enteFile, Map indexedFileIds) {
if (_isIndexingOrClusteringRunning == false ||
_mlControllerStatus == false) {
diff --git a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart
index db1713c2c3..1384750811 100644
--- a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart
+++ b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart
@@ -23,6 +23,7 @@ import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx
import "package:photos/utils/debouncer.dart";
import "package:photos/utils/device_info.dart";
import "package:photos/utils/local_settings.dart";
+import "package:photos/utils/ml_util.dart";
import "package:photos/utils/thumbnail_util.dart";
class SemanticSearchService {
@@ -160,8 +161,7 @@ class SemanticSearchService {
}
Future getIndexStatus() async {
- final indexableFileIDs = await FilesDB.instance
- .getOwnedFileIDs(Configuration.instance.getUserID()!);
+ final indexableFileIDs = await getIndexableFileIDs();
return IndexStatus(
min(_cachedEmbeddings.length, indexableFileIDs.length),
(await _getFileIDsToBeIndexed()).length,
@@ -222,8 +222,7 @@ class SemanticSearchService {
}
Future> _getFileIDsToBeIndexed() async {
- final uploadedFileIDs = await FilesDB.instance
- .getOwnedFileIDs(Configuration.instance.getUserID()!);
+ final uploadedFileIDs = await getIndexableFileIDs();
final embeddedFileIDs =
await EmbeddingsDB.instance.getFileIDs(_currentModel);
diff --git a/mobile/lib/ui/settings/machine_learning_settings_page.dart b/mobile/lib/ui/settings/machine_learning_settings_page.dart
index 83bc159a53..cf546015ce 100644
--- a/mobile/lib/ui/settings/machine_learning_settings_page.dart
+++ b/mobile/lib/ui/settings/machine_learning_settings_page.dart
@@ -26,6 +26,7 @@ import "package:photos/ui/components/title_bar_widget.dart";
import "package:photos/ui/components/toggle_switch_widget.dart";
import "package:photos/utils/data_util.dart";
import "package:photos/utils/local_settings.dart";
+import "package:photos/utils/ml_util.dart";
final _logger = Logger("MachineLearningSettingsPage");
@@ -442,7 +443,7 @@ class FaceRecognitionStatusWidgetState
try {
final indexedFiles = await FaceMLDataDB.instance
.getIndexedFileCount(minimumMlVersion: faceMlVersion);
- final indexableFiles = (await FaceMlService.getIndexableFileIDs()).length;
+ final indexableFiles = (await getIndexableFileIDs()).length;
final showIndexedFiles = min(indexedFiles, indexableFiles);
final pendingFiles = max(indexableFiles - indexedFiles, 0);
final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount();
diff --git a/mobile/lib/utils/ml_util.dart b/mobile/lib/utils/ml_util.dart
new file mode 100644
index 0000000000..4033e29349
--- /dev/null
+++ b/mobile/lib/utils/ml_util.dart
@@ -0,0 +1,7 @@
+import "package:photos/core/configuration.dart";
+import "package:photos/db/files_db.dart";
+
+Future> getIndexableFileIDs() async {
+ return FilesDB.instance
+ .getOwnedFileIDs(Configuration.instance.getUserID()!);
+ }
\ No newline at end of file
From 2ce921245778a031d8a817193d6b20d32474c67c Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 09:57:16 +0530
Subject: [PATCH 09/32] We encodeURIComponent the pathname
---
web/apps/auth/src/services/code.ts | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts
index 835bb689dd..a43a3d4730 100644
--- a/web/apps/auth/src/services/code.ts
+++ b/web/apps/auth/src/services/code.ts
@@ -45,6 +45,8 @@ export interface Code {
*
* - (TOTP)
* otpauth://totp/ACME:user@example.org?algorithm=SHA1&digits=6&issuer=acme&period=30&secret=ALPHANUM
+ *
+ * See also `auth/test/models/code_test.dart`.
*/
export const codeFromURIString = (id: string, uriString: string): Code => {
const url = new URL(uriString);
@@ -70,7 +72,7 @@ const parseType = (url: URL): Code["type"] => {
const parseAccount = (url: URL): string | undefined => {
// "/ACME:user@example.org" => "user@example.org"
- let p = url.pathname;
+ let p = decodeURIComponent(url.pathname);
if (p.startsWith("/")) p = p.slice(1);
if (p.includes(":")) p = p.split(":").slice(1).join(":");
return p;
@@ -89,7 +91,7 @@ const parseIssuer = (url: URL): string => {
// Otherwise use the `prefix:` from the account as the issuer.
// "/ACME:user@example.org" => "ACME"
- let p = url.pathname;
+ let p = decodeURIComponent(url.pathname);
if (p.startsWith("/")) p = p.slice(1);
if (p.includes(":")) p = p.split(":")[0];
From eaf8b9cebc770eee44d418cb7aa7a61124ae82af Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 10:10:59 +0530
Subject: [PATCH 10/32] Also include same workaround as mobile app
---
web/apps/auth/src/services/code.ts | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts
index a43a3d4730..a04409ceea 100644
--- a/web/apps/auth/src/services/code.ts
+++ b/web/apps/auth/src/services/code.ts
@@ -49,6 +49,19 @@ export interface Code {
* See also `auth/test/models/code_test.dart`.
*/
export const codeFromURIString = (id: string, uriString: string): Code => {
+ try {
+ return _codeFromURIString(id, uriString);
+ } catch (e) {
+ // We might have legacy encodings of account names that contain a "#",
+ // which causes the rest of the URL to be treated as a fragment, and
+ // ignored. See if this was potentially such a case, otherwise rethrow.
+ if (uriString.includes("#"))
+ return _codeFromURIString(id, uriString.replaceAll("#", "%23"));
+ throw e;
+ }
+};
+
+const _codeFromURIString = (id: string, uriString: string): Code => {
const url = new URL(uriString);
return {
From c3fb47228768a02fcb821d04e7e4eed5d9112240 Mon Sep 17 00:00:00 2001
From: laurenspriem
Date: Fri, 24 May 2024 10:18:17 +0530
Subject: [PATCH 11/32] [mob][photos] Fix clustering progress number
---
mobile/lib/face/db.dart | 29 +++++++++++++++----
.../debug/face_debug_section_widget.dart | 2 +-
.../machine_learning_settings_page.dart | 11 +++----
3 files changed, 28 insertions(+), 14 deletions(-)
diff --git a/mobile/lib/face/db.dart b/mobile/lib/face/db.dart
index c72b197b46..626a25114d 100644
--- a/mobile/lib/face/db.dart
+++ b/mobile/lib/face/db.dart
@@ -13,6 +13,8 @@ import "package:photos/face/model/face.dart";
import "package:photos/models/file/file.dart";
import "package:photos/services/machine_learning/face_ml/face_clustering/face_info_for_clustering.dart";
import 'package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart';
+import "package:photos/services/machine_learning/face_ml/face_ml_result.dart";
+import "package:photos/utils/ml_util.dart";
import 'package:sqlite_async/sqlite_async.dart';
/// Stores all data for the FacesML-related features. The database can be accessed by `FaceMLDataDB.instance.database`.
@@ -401,8 +403,10 @@ class FaceMLDataDB {
final personID = map[personIdColumn] as String;
final clusterID = map[fcClusterID] as int;
final faceID = map[fcFaceId] as String;
- result.putIfAbsent(personID, () => {}).putIfAbsent(clusterID, () => {})
- .add(faceID);
+ result
+ .putIfAbsent(personID, () => {})
+ .putIfAbsent(clusterID, () => {})
+ .add(faceID);
}
return result;
}
@@ -673,11 +677,24 @@ class FaceMLDataDB {
return maps.first['count'] as int;
}
- Future getClusteredToTotalFacesRatio() async {
- final int totalFaces = await getTotalFaceCount();
- final int clusteredFaces = await getClusteredFaceCount();
+ Future getClusteredFileCount() async {
+ final db = await instance.asyncDB;
+ final List> maps = await db.getAll(
+ 'SELECT COUNT(DISTINCT $fcFaceId) as count FROM $faceClustersTable',
+ );
+ final Set fileIDs = {};
+ for (final map in maps) {
+ final int fileID = getFileIdFromFaceId(map[fcFaceId] as String);
+ fileIDs.add(fileID);
+ }
+ return fileIDs.length;
+ }
- return clusteredFaces / totalFaces;
+ Future getClusteredToIndexableFilesRatio() async {
+ final int indexableFiles = (await getIndexableFileIDs()).length;
+ final int clusteredFiles = await getClusteredFileCount();
+
+ return clusteredFiles / indexableFiles;
}
Future getBlurryFaceCount([
diff --git a/mobile/lib/ui/settings/debug/face_debug_section_widget.dart b/mobile/lib/ui/settings/debug/face_debug_section_widget.dart
index 726a9f2ceb..376793769f 100644
--- a/mobile/lib/ui/settings/debug/face_debug_section_widget.dart
+++ b/mobile/lib/ui/settings/debug/face_debug_section_widget.dart
@@ -177,7 +177,7 @@ class _FaceDebugSectionWidgetState extends State {
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: FutureBuilder(
- future: FaceMLDataDB.instance.getClusteredToTotalFacesRatio(),
+ future: FaceMLDataDB.instance.getClusteredToIndexableFilesRatio(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return CaptionedTextWidget(
diff --git a/mobile/lib/ui/settings/machine_learning_settings_page.dart b/mobile/lib/ui/settings/machine_learning_settings_page.dart
index cf546015ce..4e9178a57d 100644
--- a/mobile/lib/ui/settings/machine_learning_settings_page.dart
+++ b/mobile/lib/ui/settings/machine_learning_settings_page.dart
@@ -439,19 +439,16 @@ class FaceRecognitionStatusWidgetState
});
}
- Future<(int, int, int, double)> getIndexStatus() async {
+ Future<(int, int, double)> getIndexStatus() async {
try {
final indexedFiles = await FaceMLDataDB.instance
.getIndexedFileCount(minimumMlVersion: faceMlVersion);
final indexableFiles = (await getIndexableFileIDs()).length;
final showIndexedFiles = min(indexedFiles, indexableFiles);
final pendingFiles = max(indexableFiles - indexedFiles, 0);
- final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount();
- final clusteredFaces =
- await FaceMLDataDB.instance.getClusteredFaceCount();
- final clusteringDoneRatio = clusteredFaces / foundFaces;
+ final clusteringDoneRatio = await FaceMLDataDB.instance.getClusteredToIndexableFilesRatio();
- return (showIndexedFiles, pendingFiles, foundFaces, clusteringDoneRatio);
+ return (showIndexedFiles, pendingFiles, clusteringDoneRatio);
} catch (e, s) {
_logger.severe('Error getting face recognition status', e, s);
rethrow;
@@ -480,7 +477,7 @@ class FaceRecognitionStatusWidgetState
if (snapshot.hasData) {
final int indexedFiles = snapshot.data!.$1;
final int pendingFiles = snapshot.data!.$2;
- final double clusteringDoneRatio = snapshot.data!.$4;
+ final double clusteringDoneRatio = snapshot.data!.$3;
final double clusteringPercentage =
(clusteringDoneRatio * 100).clamp(0, 100);
From 86f96a571326e1a39d5da6e748a774d0fb0c5553 Mon Sep 17 00:00:00 2001
From: laurenspriem
Date: Fri, 24 May 2024 10:19:24 +0530
Subject: [PATCH 12/32] [mob][photos] Show intermediate clustering results
---
mobile/lib/face/db.dart | 8 --------
mobile/lib/services/search_service.dart | 9 ---------
2 files changed, 17 deletions(-)
diff --git a/mobile/lib/face/db.dart b/mobile/lib/face/db.dart
index 626a25114d..17055234bd 100644
--- a/mobile/lib/face/db.dart
+++ b/mobile/lib/face/db.dart
@@ -669,14 +669,6 @@ class FaceMLDataDB {
return maps.first['count'] as int;
}
- Future getClusteredFaceCount() async {
- final db = await instance.asyncDB;
- final List> maps = await db.getAll(
- 'SELECT COUNT(DISTINCT $fcFaceId) as count FROM $faceClustersTable',
- );
- return maps.first['count'] as int;
- }
-
Future getClusteredFileCount() async {
final db = await instance.asyncDB;
final List> maps = await db.getAll(
diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart
index 1ff73dbc89..d15eddb718 100644
--- a/mobile/lib/services/search_service.dart
+++ b/mobile/lib/services/search_service.dart
@@ -754,15 +754,6 @@ class SearchService {
Future> getAllFace(int? limit) async {
try {
- // Don't return anything if clustering is not nearly complete yet
- final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount();
- final clusteredFaces =
- await FaceMLDataDB.instance.getClusteredFaceCount();
- final clusteringDoneRatio = clusteredFaces / foundFaces;
- if (clusteringDoneRatio < 0.9) {
- return [];
- }
-
debugPrint("getting faces");
final Map> fileIdToClusterID =
await FaceMLDataDB.instance.getFileIdToClusterIds();
From fec040e5285b843cad9a28bcf8ff1ec87e37dfe9 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 10:19:53 +0530
Subject: [PATCH 13/32] Tweak error report
---
web/apps/auth/src/services/code.ts | 2 +-
web/apps/auth/src/services/remote.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts
index a04409ceea..901411dd30 100644
--- a/web/apps/auth/src/services/code.ts
+++ b/web/apps/auth/src/services/code.ts
@@ -80,7 +80,7 @@ const _codeFromURIString = (id: string, uriString: string): Code => {
const parseType = (url: URL): Code["type"] => {
const t = url.host.toLowerCase();
if (t == "totp" || t == "hotp") return t;
- throw new Error(`Unsupported code with host ${t}`);
+ throw new Error(`Unsupported code with host "${t}"`);
};
const parseAccount = (url: URL): string | undefined => {
diff --git a/web/apps/auth/src/services/remote.ts b/web/apps/auth/src/services/remote.ts
index 07b15d7d71..11d57aa23b 100644
--- a/web/apps/auth/src/services/remote.ts
+++ b/web/apps/auth/src/services/remote.ts
@@ -35,7 +35,7 @@ export const getAuthCodes = async (): Promise => {
);
return codeFromURIString(entity.id, decryptedCode);
} catch (e) {
- log.error(`failed to parse codeId = ${entity.id}`);
+ log.error(`Failed to parse codeID ${entity.id}`, e);
return null;
}
}),
From edf9f743f44a9b7483e735fa4a5b042228e8993b Mon Sep 17 00:00:00 2001
From: laurenspriem
Date: Fri, 24 May 2024 10:27:16 +0530
Subject: [PATCH 14/32] [mob][photos] Prefer using getFileIdFromFaceId
---
mobile/lib/face/db.dart | 15 +++++++--------
.../machine_learning/face_ml/face_ml_result.dart | 6 +++++-
2 files changed, 12 insertions(+), 9 deletions(-)
diff --git a/mobile/lib/face/db.dart b/mobile/lib/face/db.dart
index 17055234bd..02a49ff4a8 100644
--- a/mobile/lib/face/db.dart
+++ b/mobile/lib/face/db.dart
@@ -251,7 +251,7 @@ class FaceMLDataDB {
final List fileId = [recentFileID];
int? avatarFileId;
if (avatarFaceId != null) {
- avatarFileId = int.tryParse(avatarFaceId.split('_')[0]);
+ avatarFileId = tryGetFileIdFromFaceId(avatarFaceId);
if (avatarFileId != null) {
fileId.add(avatarFileId);
}
@@ -480,8 +480,7 @@ class FaceMLDataDB {
for (final map in maps) {
final clusterID = map[fcClusterID] as int;
final faceID = map[fcFaceId] as String;
- final x = faceID.split('_').first;
- final fileID = int.parse(x);
+ final fileID = getFileIdFromFaceId(faceID);
result[fileID] = (result[fileID] ?? {})..add(clusterID);
}
return result;
@@ -804,7 +803,7 @@ class FaceMLDataDB {
for (final map in maps) {
final clusterID = map[clusterIDColumn] as int;
final String faceID = map[fcFaceId] as String;
- final fileID = int.parse(faceID.split('_').first);
+ final fileID = getFileIdFromFaceId(faceID);
result[fileID] = (result[fileID] ?? {})..add(clusterID);
}
return result;
@@ -823,8 +822,8 @@ class FaceMLDataDB {
final Map> result = {};
for (final map in maps) {
final clusterID = map[fcClusterID] as int;
- final faceId = map[fcFaceId] as String;
- final fileID = int.parse(faceId.split("_").first);
+ final faceID = map[fcFaceId] as String;
+ final fileID = getFileIdFromFaceId(faceID);
result[fileID] = (result[fileID] ?? {})..add(clusterID);
}
return result;
@@ -973,7 +972,7 @@ class FaceMLDataDB {
final Map faceIDToClusterID = {};
for (final row in faceIdsResult) {
final faceID = row[fcFaceId] as String;
- if (fileIds.contains(faceID.split('_').first)) {
+ if (fileIds.contains(getFileIdFromFaceId(faceID))) {
maxClusterID += 1;
faceIDToClusterID[faceID] = maxClusterID;
}
@@ -999,7 +998,7 @@ class FaceMLDataDB {
final Map faceIDToClusterID = {};
for (final row in faceIdsResult) {
final faceID = row[fcFaceId] as String;
- if (fileIds.contains(faceID.split('_').first)) {
+ if (fileIds.contains(getFileIdFromFaceId(faceID))) {
maxClusterID += 1;
faceIDToClusterID[faceID] = maxClusterID;
}
diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_result.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_result.dart
index 19f954013e..9f87b87220 100644
--- a/mobile/lib/services/machine_learning/face_ml/face_ml_result.dart
+++ b/mobile/lib/services/machine_learning/face_ml/face_ml_result.dart
@@ -310,5 +310,9 @@ class FaceResultBuilder {
}
int getFileIdFromFaceId(String faceId) {
- return int.parse(faceId.split("_")[0]);
+ return int.parse(faceId.split("_").first);
}
+
+int? tryGetFileIdFromFaceId(String faceId) {
+ return int.tryParse(faceId.split("_").first);
+}
\ No newline at end of file
From dc38a8bc9f851d6c7a9db3a14d36ccc7b93d211c Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 10:51:19 +0530
Subject: [PATCH 15/32] Account for node/browser discrepancy
---
web/apps/auth/src/services/code.ts | 39 +++++++++++++++++++++---------
1 file changed, 28 insertions(+), 11 deletions(-)
diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts
index 901411dd30..c0be011ea3 100644
--- a/web/apps/auth/src/services/code.ts
+++ b/web/apps/auth/src/services/code.ts
@@ -64,11 +64,27 @@ export const codeFromURIString = (id: string, uriString: string): Code => {
const _codeFromURIString = (id: string, uriString: string): Code => {
const url = new URL(uriString);
+ // A URL like
+ //
+ // new URL("otpauth://hotp/Test?secret=AAABBBCCCDDDEEEFFF&issuer=Test&counter=0")
+ //
+ // is parsed differently by the browser and Node depending on the scheme.
+ // When the scheme is http(s), then both of them consider "hotp" as the
+ // `host`. However, when the scheme is "otpauth", as is our case, the
+ // browser considers the entire thing as part of the pathname. so we get.
+ //
+ // host: ""
+ // pathname: "//hotp/Test"
+ //
+ // Since this code run on browsers only, we parse as per that behaviour.
+
+ const [type, path] = parsePathname(url);
+
return {
id,
- type: parseType(url),
- account: parseAccount(url),
- issuer: parseIssuer(url),
+ type,
+ account: parseAccount(path),
+ issuer: parseIssuer(url, path),
digits: parseDigits(url),
period: parsePeriod(url),
secret: parseSecret(url),
@@ -77,21 +93,22 @@ const _codeFromURIString = (id: string, uriString: string): Code => {
};
};
-const parseType = (url: URL): Code["type"] => {
- const t = url.host.toLowerCase();
- if (t == "totp" || t == "hotp") return t;
- throw new Error(`Unsupported code with host "${t}"`);
+const parsePathname = (url: URL): [type: Code["type"], path: string] => {
+ const p = url.pathname.toLowerCase();
+ if (p.startsWith("//totp")) return ["totp", url.pathname.slice(6)];
+ if (p.startsWith("//hotp")) return ["hotp", url.pathname.slice(6)];
+ throw new Error(`Unsupported code or unparseable path "${url.pathname}"`);
};
-const parseAccount = (url: URL): string | undefined => {
+const parseAccount = (path: string): string | undefined => {
// "/ACME:user@example.org" => "user@example.org"
- let p = decodeURIComponent(url.pathname);
+ let p = decodeURIComponent(path);
if (p.startsWith("/")) p = p.slice(1);
if (p.includes(":")) p = p.split(":").slice(1).join(":");
return p;
};
-const parseIssuer = (url: URL): string => {
+const parseIssuer = (url: URL, path: string): string => {
// If there is a "issuer" search param, use that.
let issuer = url.searchParams.get("issuer");
if (issuer) {
@@ -104,7 +121,7 @@ const parseIssuer = (url: URL): string => {
// Otherwise use the `prefix:` from the account as the issuer.
// "/ACME:user@example.org" => "ACME"
- let p = decodeURIComponent(url.pathname);
+ let p = decodeURIComponent(path);
if (p.startsWith("/")) p = p.slice(1);
if (p.includes(":")) p = p.split(":")[0];
From f1d1a4a9e1cce35eef2857ccfdd9fe004ab6bda9 Mon Sep 17 00:00:00 2001
From: laurenspriem
Date: Fri, 24 May 2024 10:57:27 +0530
Subject: [PATCH 16/32] [mob][photos] Clustering sort to cluster new files
first
---
.../face_clustering_service.dart | 56 +++++++------------
.../face_ml/face_ml_service.dart | 4 +-
2 files changed, 22 insertions(+), 38 deletions(-)
diff --git a/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart b/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart
index 1b8d9c3bd5..1a635b0f07 100644
--- a/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart
+++ b/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart
@@ -498,19 +498,8 @@ class FaceClusteringService {
}
}
- // Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first
if (fileIDToCreationTime != null) {
- faceInfos.sort((a, b) {
- if (a.fileCreationTime == null && b.fileCreationTime == null) {
- return 0;
- } else if (a.fileCreationTime == null) {
- return 1;
- } else if (b.fileCreationTime == null) {
- return -1;
- } else {
- return a.fileCreationTime!.compareTo(b.fileCreationTime!);
- }
- });
+ _sortFaceInfosOnCreationTime(faceInfos);
}
// Sort the faceInfos such that the ones with null clusterId are at the end
@@ -796,19 +785,8 @@ class FaceClusteringService {
);
}
- // Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first
if (fileIDToCreationTime != null) {
- faceInfos.sort((a, b) {
- if (a.fileCreationTime == null && b.fileCreationTime == null) {
- return 0;
- } else if (a.fileCreationTime == null) {
- return 1;
- } else if (b.fileCreationTime == null) {
- return -1;
- } else {
- return a.fileCreationTime!.compareTo(b.fileCreationTime!);
- }
- });
+ _sortFaceInfosOnCreationTime(faceInfos);
}
if (faceInfos.isEmpty) {
@@ -996,19 +974,8 @@ class FaceClusteringService {
);
}
- // Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first
if (fileIDToCreationTime != null) {
- faceInfos.sort((a, b) {
- if (a.fileCreationTime == null && b.fileCreationTime == null) {
- return 0;
- } else if (a.fileCreationTime == null) {
- return 1;
- } else if (b.fileCreationTime == null) {
- return -1;
- } else {
- return a.fileCreationTime!.compareTo(b.fileCreationTime!);
- }
- });
+ _sortFaceInfosOnCreationTime(faceInfos);
}
// Get the embeddings
@@ -1027,3 +994,20 @@ class FaceClusteringService {
return clusteredFaceIDs;
}
}
+
+/// Sort the faceInfos based on fileCreationTime, in descending order, so newest faces are first
+void _sortFaceInfosOnCreationTime(
+ List faceInfos,
+) {
+ faceInfos.sort((b, a) {
+ if (a.fileCreationTime == null && b.fileCreationTime == null) {
+ return 0;
+ } else if (a.fileCreationTime == null) {
+ return 1;
+ } else if (b.fileCreationTime == null) {
+ return -1;
+ } else {
+ return a.fileCreationTime!.compareTo(b.fileCreationTime!);
+ }
+ });
+}
diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
index 9bfa54f1ea..47464b8e9f 100644
--- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
+++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
@@ -590,8 +590,8 @@ class FaceMlService {
allFaceInfoForClustering.add(faceInfo);
}
}
- // sort the embeddings based on file creation time, oldest first
- allFaceInfoForClustering.sort((a, b) {
+ // sort the embeddings based on file creation time, newest first
+ allFaceInfoForClustering.sort((b, a) {
return fileIDToCreationTime[a.fileID]!
.compareTo(fileIDToCreationTime[b.fileID]!);
});
From 5587373b422a6454fdf520b0e15102adc17c6ae3 Mon Sep 17 00:00:00 2001
From: laurenspriem
Date: Fri, 24 May 2024 11:00:05 +0530
Subject: [PATCH 17/32] [mob][photos] Remove clustering restriction based on
indexed amount
---
.../face_ml/face_ml_service.dart | 24 +------------------
1 file changed, 1 insertion(+), 23 deletions(-)
diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
index 47464b8e9f..932c7e39e1 100644
--- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
+++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
@@ -47,7 +47,6 @@ import "package:photos/utils/file_util.dart";
import 'package:photos/utils/image_ml_isolate.dart';
import "package:photos/utils/image_ml_util.dart";
import "package:photos/utils/local_settings.dart";
-import "package:photos/utils/ml_util.dart";
import "package:photos/utils/network_util.dart";
import "package:photos/utils/thumbnail_util.dart";
import "package:synchronized/synchronized.dart";
@@ -359,15 +358,7 @@ class FaceMlService {
await sync(forceSync: _shouldSyncPeople);
await indexAllImages();
- final indexingCompleteRatio = await _getIndexedDoneRatio();
- if (indexingCompleteRatio < 0.95) {
- _logger.info(
- "Indexing is not far enough to start clustering, skipping clustering. Indexing is at $indexingCompleteRatio",
- );
- return;
- } else {
- await clusterAllImages();
- }
+ await clusterAllImages();
}
void pauseIndexingAndClustering() {
@@ -1171,19 +1162,6 @@ class FaceMlService {
}
}
- Future _getIndexedDoneRatio() async {
- final w = (kDebugMode ? EnteWatch('_getIndexedDoneRatio') : null)?..start();
-
- final int alreadyIndexedCount = await FaceMLDataDB.instance
- .getIndexedFileCount(minimumMlVersion: faceMlVersion);
- final int totalIndexableCount = (await getIndexableFileIDs()).length;
- final ratio = alreadyIndexedCount / totalIndexableCount;
-
- w?.log('getIndexedDoneRatio');
-
- return ratio;
- }
-
bool _skipAnalysisEnteFile(EnteFile enteFile, Map indexedFileIds) {
if (_isIndexingOrClusteringRunning == false ||
_mlControllerStatus == false) {
From cc91cb8012a38df370807f81a3fbb90df39d229a Mon Sep 17 00:00:00 2001
From: laurenspriem
Date: Fri, 24 May 2024 11:16:40 +0530
Subject: [PATCH 18/32] [mob][photos] Correct mistake
---
mobile/lib/face/db.dart | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mobile/lib/face/db.dart b/mobile/lib/face/db.dart
index 02a49ff4a8..e9e6079204 100644
--- a/mobile/lib/face/db.dart
+++ b/mobile/lib/face/db.dart
@@ -671,7 +671,7 @@ class FaceMLDataDB {
Future getClusteredFileCount() async {
final db = await instance.asyncDB;
final List> maps = await db.getAll(
- 'SELECT COUNT(DISTINCT $fcFaceId) as count FROM $faceClustersTable',
+ 'SELECT $fcFaceId FROM $faceClustersTable',
);
final Set fileIDs = {};
for (final map in maps) {
From 697946f4154977ad16b053cca774e702ac30d620 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 12:12:06 +0530
Subject: [PATCH 19/32] Scaffold
---
web/apps/auth/src/services/code.ts | 40 +++++++++++++++++++++++++++++-
1 file changed, 39 insertions(+), 1 deletion(-)
diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts
index c0be011ea3..b02e2314b8 100644
--- a/web/apps/auth/src/services/code.ts
+++ b/web/apps/auth/src/services/code.ts
@@ -171,7 +171,7 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => {
});
otp = totp.generate();
nextOTP = totp.generate({
- timestamp: new Date().getTime() + code.period * 1000,
+ timestamp: Date.now() + code.period * 1000,
});
break;
}
@@ -186,6 +186,44 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => {
nextOTP = hotp.generate({ counter: 1 });
break;
}
+
+ case "steam": {
+ const steam = new Steam({
+ secret: code.secret,
+ });
+ otp = steam.generate();
+ nextOTP = steam.generate({
+ timestamp: Date.now() + code.period * 1000,
+ });
+ break;
+ }
}
return [otp, nextOTP];
};
+
+/**
+ * Steam OTPs.
+ *
+ * Steam's algorithm is a custom variant of TOTP that uses a 26-character
+ * alphabet instead of digits.
+ *
+ * A Dart implementation of the algorithm can be found in
+ * https://github.com/elliotwutingfeng/steam_totp/blob/main/lib/src/steam_totp_base.dart
+ * (MIT license), and we use that as a reference. Our implementation is written
+ * in the style of the other TOTP/HOTP classes that are provided by the otpauth
+ * JS library that we use for the normal TOTP/HOTP generation
+ * https://github.com/hectorm/otpauth/blob/master/src/hotp.js (MIT license).
+ */
+class Steam {
+ secret: string;
+ period: number;
+
+ constructor({ secret }: { secret: string }) {
+ this.secret = secret;
+ this.period = 30;
+ }
+
+ generate({ timestamp = Date.now() }: { timestamp: number }) {
+ return `${timestamp}`;
+ }
+}
From 1ce90839fe8e43a4f4d931126881aec604e8e65b Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 12:18:28 +0530
Subject: [PATCH 20/32] Remove type from auth UI
---
web/apps/auth/src/pages/auth.tsx | 23 ++++++++---------------
1 file changed, 8 insertions(+), 15 deletions(-)
diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx
index 5264897795..d750c5f7c7 100644
--- a/web/apps/auth/src/pages/auth.tsx
+++ b/web/apps/auth/src/pages/auth.tsx
@@ -187,28 +187,21 @@ const CodeDisplay: React.FC = ({ code }) => {
useEffect(() => {
// Generate to set the initial otp and nextOTP on component mount.
regen();
- const codeType = code.type;
- const codePeriodInMs = code.period * 1000;
- const timeToNextCode =
- codePeriodInMs - (new Date().getTime() % codePeriodInMs);
- const interval = null;
+
+ const periodMs = code.period * 1000;
+ const timeToNextCode = periodMs - (Date.now() % periodMs);
+
+ let interval: ReturnType | undefined;
// Wait until we are at the start of the next code period, and then
// start the interval loop.
setTimeout(() => {
// We need to call regen() once before the interval loop to set the
// initial otp and nextOTP.
regen();
- codeType.toLowerCase() === "totp" ||
- codeType.toLowerCase() === "hotp"
- ? setInterval(() => {
- regen();
- }, codePeriodInMs)
- : null;
+ interval = setInterval(() => regen, periodMs);
}, timeToNextCode);
- return () => {
- if (interval) clearInterval(interval);
- };
+ return () => interval && clearInterval(interval);
}, [code]);
return (
@@ -346,7 +339,7 @@ const TimerProgress: React.FC = ({ period }) => {
useEffect(() => {
const advance = () => {
- const timeRemaining = us - ((new Date().getTime() * 1000) % us);
+ const timeRemaining = us - ((Date.now() * 1000) % us);
setProgress(timeRemaining / us);
};
From 0fdb58eda19095d70fa010a429ba9c0a062a41e8 Mon Sep 17 00:00:00 2001
From: laurenspriem
Date: Fri, 24 May 2024 12:30:22 +0530
Subject: [PATCH 21/32] [mob][photos] Force clustering first if too many
unclustered faces
---
mobile/lib/face/db.dart | 14 ++++++++++++++
.../machine_learning/face_ml/face_ml_service.dart | 10 ++++++++++
2 files changed, 24 insertions(+)
diff --git a/mobile/lib/face/db.dart b/mobile/lib/face/db.dart
index e9e6079204..9b5f42f540 100644
--- a/mobile/lib/face/db.dart
+++ b/mobile/lib/face/db.dart
@@ -688,6 +688,20 @@ class FaceMLDataDB {
return clusteredFiles / indexableFiles;
}
+ Future getUnclusteredFaceCount() async {
+ final db = await instance.asyncDB;
+ const String query = '''
+ SELECT f.$faceIDColumn
+ FROM $facesTable f
+ LEFT JOIN $faceClustersTable fc ON f.$faceIDColumn = fc.$fcFaceId
+ WHERE f.$faceScore > $kMinimumQualityFaceScore
+ AND f.$faceBlur > $kLaplacianHardThreshold
+ AND fc.$fcFaceId IS NULL
+ ''';
+ final List> maps = await db.getAll(query);
+ return maps.length;
+ }
+
Future getBlurryFaceCount([
int blurThreshold = kLaplacianHardThreshold,
]) async {
diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
index 932c7e39e1..a6b38f8e3b 100644
--- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
+++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
@@ -98,6 +98,7 @@ class FaceMlService {
final int _fileDownloadLimit = 5;
final int _embeddingFetchLimit = 200;
+ final int _kForceClusteringFaceCount = 4000;
Future init({bool initializeImageMlIsolate = false}) async {
if (LocalSettings.instance.isFaceIndexingEnabled == false) {
@@ -357,6 +358,15 @@ class FaceMlService {
if (_cannotRunMLFunction()) return;
await sync(forceSync: _shouldSyncPeople);
+
+ final int unclusteredFacesCount =
+ await FaceMLDataDB.instance.getUnclusteredFaceCount();
+ if (unclusteredFacesCount > _kForceClusteringFaceCount) {
+ _logger.info(
+ "There are $unclusteredFacesCount unclustered faces, doing clustering first",
+ );
+ await clusterAllImages();
+ }
await indexAllImages();
await clusterAllImages();
}
From 05e737cb11b90dc92f4b6dac46fe7bfdb6885c57 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 12:32:58 +0530
Subject: [PATCH 22/32] Add steam as a type
---
web/apps/auth/src/services/code.ts | 27 +++++++++++++++++++++------
1 file changed, 21 insertions(+), 6 deletions(-)
diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts
index b02e2314b8..74d982849b 100644
--- a/web/apps/auth/src/services/code.ts
+++ b/web/apps/auth/src/services/code.ts
@@ -10,13 +10,19 @@ export interface Code {
/** A unique id for the corresponding "auth entity" in our system. */
id?: String;
/** The type of the code. */
- type: "totp" | "hotp";
+ type: "totp" | "hotp" | "steam";
/** The user's account or email for which this code is used. */
account?: string;
/** The name of the entity that issued this code. */
issuer: string;
- /** Number of digits in the generated OTP. */
- digits: number;
+ /**
+ * Length of the generated OTP.
+ *
+ * This is vernacularly called "digits", which is an accurate description
+ * for the OG TOTP/HOTP codes. However, steam codes are not just digits, so
+ * we name this as a content-neutral "length".
+ */
+ length: number;
/**
* The time period (in seconds) for which a single OTP generated from this
* code remains valid.
@@ -85,7 +91,7 @@ const _codeFromURIString = (id: string, uriString: string): Code => {
type,
account: parseAccount(path),
issuer: parseIssuer(url, path),
- digits: parseDigits(url),
+ length: parseLength(url),
period: parsePeriod(url),
secret: parseSecret(url),
algorithm: parseAlgorithm(url),
@@ -130,8 +136,17 @@ const parseIssuer = (url: URL, path: string): string => {
return p;
};
-const parseDigits = (url: URL): number =>
- parseInt(url.searchParams.get("digits") ?? "", 10) || 6;
+/**
+ * Parse the length of the generated code.
+ *
+ * The URI query param is called digits since originally TOTP/HOTP codes used
+ * this for generating numeric codes. Now we also support steam, which instead
+ * shows non-numeric codes, and also with a different default length of 5.
+ */
+const parseLength = (url: URL, type: Code["type"]): number => {
+ const defaultLength = type == "steam" ? 5 : 6;
+ return parseInt(url.searchParams.get("digits") ?? "", 10) || defaultLength;
+};
const parsePeriod = (url: URL): number =>
parseInt(url.searchParams.get("period") ?? "", 10) || 30;
From 370b28f9e4d60839cf0b9a1f5367827dd18af91c Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 12:38:35 +0530
Subject: [PATCH 23/32] Type
---
web/apps/auth/src/services/code.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts
index 74d982849b..ac71a9adcf 100644
--- a/web/apps/auth/src/services/code.ts
+++ b/web/apps/auth/src/services/code.ts
@@ -91,7 +91,7 @@ const _codeFromURIString = (id: string, uriString: string): Code => {
type,
account: parseAccount(path),
issuer: parseIssuer(url, path),
- length: parseLength(url),
+ length: parseLength(url, type),
period: parsePeriod(url),
secret: parseSecret(url),
algorithm: parseAlgorithm(url),
@@ -182,7 +182,7 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => {
secret: code.secret,
algorithm: code.algorithm,
period: code.period,
- digits: code.digits,
+ digits: code.length,
});
otp = totp.generate();
nextOTP = totp.generate({
@@ -238,7 +238,7 @@ class Steam {
this.period = 30;
}
- generate({ timestamp = Date.now() }: { timestamp: number }) {
+ generate({ timestamp }: { timestamp: number } = { timestamp: Date.now() }) {
return `${timestamp}`;
}
}
From ef6fe80944e5467e7a07a03a42a115bc1802ee17 Mon Sep 17 00:00:00 2001
From: laurenspriem
Date: Fri, 24 May 2024 12:44:01 +0530
Subject: [PATCH 24/32] [mob][photos] Fix 400 on embedding fetch
---
.../services/machine_learning/face_ml/face_ml_service.dart | 2 +-
.../machine_learning/file_ml/remote_fileml_service.dart | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
index a6b38f8e3b..5f3d15bdc4 100644
--- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
+++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart
@@ -446,7 +446,7 @@ class FaceMlService {
if (LocalSettings.instance.remoteFetchEnabled) {
try {
- final List fileIds = [];
+ final Set fileIds = {}; // if there are duplicates here server returns 400
// Try to find embeddings on the remote server
for (final f in chunk) {
fileIds.add(f.uploadedFileID!);
diff --git a/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart b/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart
index bb1c9f999b..4712916d07 100644
--- a/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart
+++ b/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart
@@ -55,13 +55,13 @@ class RemoteFileMLService {
}
Future getFilessEmbedding(
- List fileIds,
+ Set fileIds,
) async {
try {
final res = await _dio.post(
"/embeddings/files",
data: {
- "fileIDs": fileIds,
+ "fileIDs": fileIds.toList(),
"model": 'file-ml-clip-face',
},
);
From 7f49f530c5515f64f87b7dfce3dd3bba5f747cb4 Mon Sep 17 00:00:00 2001
From: laurenspriem
Date: Fri, 24 May 2024 12:47:10 +0530
Subject: [PATCH 25/32] [mob][photos] Bump
---
mobile/pubspec.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index a1f6607763..1417d17f3e 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 0.8.109+633
+version: 0.8.110+634
publish_to: none
environment:
From 36aa33ed5a00804a1b272bf2a2e401982150a220 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 13:08:41 +0530
Subject: [PATCH 26/32] Move to separate file
---
web/apps/auth/package.json | 1 +
web/apps/auth/src/services/code.ts | 27 -----------------
web/apps/auth/src/services/steam.ts | 46 +++++++++++++++++++++++++++++
web/docs/dependencies.md | 4 +++
4 files changed, 51 insertions(+), 27 deletions(-)
create mode 100644 web/apps/auth/src/services/steam.ts
diff --git a/web/apps/auth/package.json b/web/apps/auth/package.json
index 463ff06e8d..268f6f5c68 100644
--- a/web/apps/auth/package.json
+++ b/web/apps/auth/package.json
@@ -7,6 +7,7 @@
"@ente/accounts": "*",
"@ente/eslint-config": "*",
"@ente/shared": "*",
+ "jssha": "~3.3.1",
"otpauth": "^9"
}
}
diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts
index ac71a9adcf..b5497f6726 100644
--- a/web/apps/auth/src/services/code.ts
+++ b/web/apps/auth/src/services/code.ts
@@ -215,30 +215,3 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => {
}
return [otp, nextOTP];
};
-
-/**
- * Steam OTPs.
- *
- * Steam's algorithm is a custom variant of TOTP that uses a 26-character
- * alphabet instead of digits.
- *
- * A Dart implementation of the algorithm can be found in
- * https://github.com/elliotwutingfeng/steam_totp/blob/main/lib/src/steam_totp_base.dart
- * (MIT license), and we use that as a reference. Our implementation is written
- * in the style of the other TOTP/HOTP classes that are provided by the otpauth
- * JS library that we use for the normal TOTP/HOTP generation
- * https://github.com/hectorm/otpauth/blob/master/src/hotp.js (MIT license).
- */
-class Steam {
- secret: string;
- period: number;
-
- constructor({ secret }: { secret: string }) {
- this.secret = secret;
- this.period = 30;
- }
-
- generate({ timestamp }: { timestamp: number } = { timestamp: Date.now() }) {
- return `${timestamp}`;
- }
-}
diff --git a/web/apps/auth/src/services/steam.ts b/web/apps/auth/src/services/steam.ts
new file mode 100644
index 0000000000..e6007287f8
--- /dev/null
+++ b/web/apps/auth/src/services/steam.ts
@@ -0,0 +1,46 @@
+import jsSHA from "jssha";
+
+/**
+ * Steam OTPs.
+ *
+ * Steam's algorithm is a custom variant of TOTP that uses a 26-character
+ * alphabet instead of digits.
+ *
+ * A Dart implementation of the algorithm can be found in
+ * https://github.com/elliotwutingfeng/steam_totp/blob/main/lib/src/steam_totp_base.dart
+ * (MIT license), and we use that as a reference. Our implementation is written
+ * in the style of the other TOTP/HOTP classes that are provided by the otpauth
+ * JS library that we use for the normal TOTP/HOTP generation
+ * https://github.com/hectorm/otpauth/blob/master/src/hotp.js (MIT license).
+ */
+export class Steam {
+ secret: string;
+ period: number;
+
+ constructor({ secret }: { secret: string }) {
+ this.secret = secret;
+ this.period = 30;
+ }
+
+ async generate(
+ { timestamp }: { timestamp: number } = { timestamp: Date.now() },
+ ) {
+ const counter = Math.floor(timestamp / 1000 / this.period);
+ // const digest = new Uint8Array(
+ // sha1HMACDigest(this.secret, uintToBuf(counter)),
+ // );
+
+ return `${timestamp}`;
+ }
+}
+
+// We don't necessarily need this dependency, we could use SubtleCrypto here
+// instead too. However, SubtleCrypto has an async interface, and we already
+// have a transitive dependency on jssha via otpauth, so just using it here
+// doesn't increase our bundle size any further.
+const sha1HMACDiggest = (key: ArrayBuffer, message: ArrayBuffer) => {
+ const hmac = new jsSHA("SHA-1", "ARRAYBUFFER");
+ hmac.setHMACKey(key, "ARRAYBUFFER");
+ hmac.update(message);
+ return hmac.getHMAC("ARRAYBUFFER");
+};
diff --git a/web/docs/dependencies.md b/web/docs/dependencies.md
index 83a2b27990..3ea8fb2409 100644
--- a/web/docs/dependencies.md
+++ b/web/docs/dependencies.md
@@ -198,3 +198,7 @@ some cases.
- [otpauth](https://github.com/hectorm/otpauth) is used for the generation of
the actual OTP from the user's TOTP/HOTP secret.
+
+- However, otpauth doesn't support steam OTPs. For these, we need to compute
+ the SHA-1, and we use the same library, `jssha` that `otpauth` uses (since
+ it is already part of our bundle).
From f6c40ee67d2799390cead23b63f20b698a7bb298 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 13:18:27 +0530
Subject: [PATCH 27/32] fromBase32 is exposed in the library API
---
web/apps/auth/src/services/code.ts | 1 +
web/apps/auth/src/services/steam.ts | 17 ++++++++---------
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts
index b5497f6726..4b0a49d88e 100644
--- a/web/apps/auth/src/services/code.ts
+++ b/web/apps/auth/src/services/code.ts
@@ -1,5 +1,6 @@
import { ensure } from "@/utils/ensure";
import { HOTP, TOTP } from "otpauth";
+import { Steam } from "./steam";
/**
* A parsed representation of an *OTP code URI.
diff --git a/web/apps/auth/src/services/steam.ts b/web/apps/auth/src/services/steam.ts
index e6007287f8..d43ec81a86 100644
--- a/web/apps/auth/src/services/steam.ts
+++ b/web/apps/auth/src/services/steam.ts
@@ -1,4 +1,5 @@
import jsSHA from "jssha";
+import { Secret } from "otpauth";
/**
* Steam OTPs.
@@ -14,21 +15,19 @@ import jsSHA from "jssha";
* https://github.com/hectorm/otpauth/blob/master/src/hotp.js (MIT license).
*/
export class Steam {
- secret: string;
+ secret: Secret;
period: number;
constructor({ secret }: { secret: string }) {
- this.secret = secret;
+ this.secret = Secret.fromBase32(secret);
this.period = 30;
}
- async generate(
- { timestamp }: { timestamp: number } = { timestamp: Date.now() },
- ) {
+ generate({ timestamp }: { timestamp: number } = { timestamp: Date.now() }) {
const counter = Math.floor(timestamp / 1000 / this.period);
- // const digest = new Uint8Array(
- // sha1HMACDigest(this.secret, uintToBuf(counter)),
- // );
+ const digest = new Uint8Array(
+ sha1HMACDigest(this.secret.buffer), uintToBuf(counter)),
+ );
return `${timestamp}`;
}
@@ -38,7 +37,7 @@ export class Steam {
// instead too. However, SubtleCrypto has an async interface, and we already
// have a transitive dependency on jssha via otpauth, so just using it here
// doesn't increase our bundle size any further.
-const sha1HMACDiggest = (key: ArrayBuffer, message: ArrayBuffer) => {
+const sha1HMACDigest = (key: ArrayBuffer, message: ArrayBuffer) => {
const hmac = new jsSHA("SHA-1", "ARRAYBUFFER");
hmac.setHMACKey(key, "ARRAYBUFFER");
hmac.update(message);
From 6594db9393e3d93eda5cddec9d3c4d807539b480 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 13:26:16 +0530
Subject: [PATCH 28/32] Encode counter
---
web/apps/auth/src/services/steam.ts | 17 ++++++++++++++---
1 file changed, 14 insertions(+), 3 deletions(-)
diff --git a/web/apps/auth/src/services/steam.ts b/web/apps/auth/src/services/steam.ts
index d43ec81a86..f53259687b 100644
--- a/web/apps/auth/src/services/steam.ts
+++ b/web/apps/auth/src/services/steam.ts
@@ -26,19 +26,30 @@ export class Steam {
generate({ timestamp }: { timestamp: number } = { timestamp: Date.now() }) {
const counter = Math.floor(timestamp / 1000 / this.period);
const digest = new Uint8Array(
- sha1HMACDigest(this.secret.buffer), uintToBuf(counter)),
+ sha1HMACDigest(this.secret.buffer, uintToArray(counter)),
);
return `${timestamp}`;
}
}
+// Equivalent to
+// https://github.com/hectorm/otpauth/blob/master/src/utils/encoding/uint.js
+const uintToArray = (n: number): Uint8Array => {
+ const result = new Uint8Array(8);
+ for (let i = 7; i >= 0; i--) {
+ result[i] = n & 0xff;
+ n >>= 8;
+ }
+ return result;
+};
+
// We don't necessarily need this dependency, we could use SubtleCrypto here
// instead too. However, SubtleCrypto has an async interface, and we already
// have a transitive dependency on jssha via otpauth, so just using it here
// doesn't increase our bundle size any further.
-const sha1HMACDigest = (key: ArrayBuffer, message: ArrayBuffer) => {
- const hmac = new jsSHA("SHA-1", "ARRAYBUFFER");
+const sha1HMACDigest = (key: ArrayBuffer, message: Uint8Array) => {
+ const hmac = new jsSHA("SHA-1", "UINT8ARRAY");
hmac.setHMACKey(key, "ARRAYBUFFER");
hmac.update(message);
return hmac.getHMAC("ARRAYBUFFER");
From cb78c848d664924b0a2445da94a897518ce55b21 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 13:36:23 +0530
Subject: [PATCH 29/32] Impl
---
web/apps/auth/src/services/steam.ts | 30 +++++++++++++++++++++++------
1 file changed, 24 insertions(+), 6 deletions(-)
diff --git a/web/apps/auth/src/services/steam.ts b/web/apps/auth/src/services/steam.ts
index f53259687b..8a9fcdf183 100644
--- a/web/apps/auth/src/services/steam.ts
+++ b/web/apps/auth/src/services/steam.ts
@@ -24,12 +24,30 @@ export class Steam {
}
generate({ timestamp }: { timestamp: number } = { timestamp: Date.now() }) {
+ // Same as regular TOTP.
const counter = Math.floor(timestamp / 1000 / this.period);
- const digest = new Uint8Array(
- sha1HMACDigest(this.secret.buffer, uintToArray(counter)),
- );
- return `${timestamp}`;
+ // Same as regular HOTP, but algorithm is fixed to SHA-1.
+ const digest = sha1HMACDigest(this.secret.buffer, uintToArray(counter));
+
+ // Same calculation as regular HOTP.
+ const offset = digest[digest.length - 1] & 15;
+ let otp =
+ ((digest[offset] & 127) << 24) |
+ ((digest[offset + 1] & 255) << 16) |
+ ((digest[offset + 2] & 255) << 8) |
+ (digest[offset + 3] & 255);
+
+ // However, instead of using this as the OTP, use it to index into
+ // the steam OTP alphabet.
+ const alphabet = "23456789BCDFGHJKMNPQRTVWXY";
+ const N = alphabet.length;
+ const steamOTP = [];
+ for (let i = 0; i < 5; i++) {
+ steamOTP.push(alphabet[otp % N]);
+ otp = Math.trunc(otp / N);
+ }
+ return steamOTP.join("");
}
}
@@ -38,7 +56,7 @@ export class Steam {
const uintToArray = (n: number): Uint8Array => {
const result = new Uint8Array(8);
for (let i = 7; i >= 0; i--) {
- result[i] = n & 0xff;
+ result[i] = n & 255;
n >>= 8;
}
return result;
@@ -52,5 +70,5 @@ const sha1HMACDigest = (key: ArrayBuffer, message: Uint8Array) => {
const hmac = new jsSHA("SHA-1", "UINT8ARRAY");
hmac.setHMACKey(key, "ARRAYBUFFER");
hmac.update(message);
- return hmac.getHMAC("ARRAYBUFFER");
+ return hmac.getHMAC("UINT8ARRAY");
};
From 0ec75c2435ab744cced4e8c7e52de1ff0db4b157 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 13:47:11 +0530
Subject: [PATCH 30/32] Parse the type
---
web/apps/auth/src/services/code.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts
index 4b0a49d88e..b5da0ffe55 100644
--- a/web/apps/auth/src/services/code.ts
+++ b/web/apps/auth/src/services/code.ts
@@ -104,6 +104,7 @@ const parsePathname = (url: URL): [type: Code["type"], path: string] => {
const p = url.pathname.toLowerCase();
if (p.startsWith("//totp")) return ["totp", url.pathname.slice(6)];
if (p.startsWith("//hotp")) return ["hotp", url.pathname.slice(6)];
+ if (p.startsWith("//steam")) return ["steam", url.pathname.slice(7)];
throw new Error(`Unsupported code or unparseable path "${url.pathname}"`);
};
From fffe96a4c71bed81ced7dbad329857372d6a6805 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 13:49:21 +0530
Subject: [PATCH 31/32] Tweak
---
web/apps/auth/src/services/steam.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/web/apps/auth/src/services/steam.ts b/web/apps/auth/src/services/steam.ts
index 8a9fcdf183..f214640c24 100644
--- a/web/apps/auth/src/services/steam.ts
+++ b/web/apps/auth/src/services/steam.ts
@@ -62,10 +62,10 @@ const uintToArray = (n: number): Uint8Array => {
return result;
};
-// We don't necessarily need this dependency, we could use SubtleCrypto here
-// instead too. However, SubtleCrypto has an async interface, and we already
-// have a transitive dependency on jssha via otpauth, so just using it here
-// doesn't increase our bundle size any further.
+// We don't necessarily need a dependency on `jssha`, we could use SubtleCrypto
+// here too. However, SubtleCrypto has an async interface, and we already have a
+// transitive dependency on `jssha` via `otpauth`, so just using it here doesn't
+// increase our bundle size any further.
const sha1HMACDigest = (key: ArrayBuffer, message: Uint8Array) => {
const hmac = new jsSHA("SHA-1", "UINT8ARRAY");
hmac.setHMACKey(key, "ARRAYBUFFER");
From bd2444d353b9517997d41079c8c477a9f95cf12a Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 24 May 2024 14:11:05 +0530
Subject: [PATCH 32/32] [web] Fix auth ticker
---
web/apps/auth/src/pages/auth.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx
index d750c5f7c7..f6661b1b13 100644
--- a/web/apps/auth/src/pages/auth.tsx
+++ b/web/apps/auth/src/pages/auth.tsx
@@ -198,7 +198,7 @@ const CodeDisplay: React.FC = ({ code }) => {
// We need to call regen() once before the interval loop to set the
// initial otp and nextOTP.
regen();
- interval = setInterval(() => regen, periodMs);
+ interval = setInterval(regen, periodMs);
}, timeToNextCode);
return () => interval && clearInterval(interval);