Skip to content

Commit dfef072

Browse files
szaimenclaude
andcommitted
feat: Add ExApp classification backend (dedicated devices)
Adds an option to offload Recognize's machine-learning classification to an External App (ExApp) container instead of running Node.js locally, so inference can run on a different, more powerful machine, optionally with a GPU. The classifiers keep running the same classifier_<model>.js scripts; a new `classifier.backend` setting routes their execution to the Recognize ExApp via AppAPI rather than to a local process, keeping results compatible. - Classifier::classifyFiles dispatches to runLocally (unchanged) or runExApp - ExAppService wraps OCA\AppAPI\PublicFunctions (optional dependency, degrades gracefully when AppAPI is not installed) - New settings classifier.backend + exapp.id, admin UI backend selector, and a /admin/exapp reachability test endpoint - exapp/ ships the App Store-installable ExApp: self-contained Dockerfile (CPU/GPU), HTTP server reusing the classifier scripts, manifest, packaging and a functional test - CI workflows to build/push the image and publish the manifest to the App Store Fixes #73 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Simon L. <szaimen@e.mail.de>
1 parent 09f7dcc commit dfef072

17 files changed

Lines changed: 1292 additions & 29 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
#
4+
# Packages the Recognize ExApp manifest and uploads it to the Nextcloud App Store.
5+
# The Docker image itself is built and pushed by exapp-publish-docker.yml; the App
6+
# Store only stores the manifest (appinfo/info.xml) and points at the registry.
7+
8+
name: Build and publish Recognize ExApp
9+
10+
on:
11+
release:
12+
types: [published]
13+
workflow_dispatch:
14+
15+
permissions:
16+
contents: write
17+
18+
env:
19+
APP_ID: recognize_exapp
20+
21+
jobs:
22+
build_and_publish:
23+
runs-on: ubuntu-latest
24+
if: ${{ github.repository_owner == 'nextcloud' }}
25+
steps:
26+
- name: Check actor permission
27+
uses: skjnldsv/check-actor-permission@v3.0
28+
with:
29+
require: write
30+
31+
- name: Checkout
32+
uses: actions/checkout@v4
33+
with:
34+
persist-credentials: false
35+
path: recognize
36+
37+
- name: Stage ExApp for packaging
38+
run: cp -r recognize/exapp "$APP_ID"
39+
40+
- name: Get app version number
41+
id: app-version
42+
uses: skjnldsv/xpath-action@v1.0.0
43+
with:
44+
filename: ${{ env.APP_ID }}/appinfo/info.xml
45+
expression: "//info//version/text()"
46+
47+
- name: Get appinfo data
48+
id: appinfo
49+
uses: skjnldsv/xpath-action@v1.0.0
50+
with:
51+
filename: ${{ env.APP_ID }}/appinfo/info.xml
52+
expression: "//info//dependencies//nextcloud/@min-version"
53+
54+
- name: Install Krankerl
55+
run: |
56+
wget -q https://github.com/ChristophWurst/krankerl/releases/download/v0.14.0/krankerl_0.14.0_amd64.deb
57+
sudo dpkg -i krankerl_0.14.0_amd64.deb
58+
59+
- name: Package ${{ env.APP_ID }} with krankerl
60+
run: |
61+
cd "$APP_ID"
62+
krankerl package
63+
64+
- name: Checkout server
65+
continue-on-error: true
66+
id: server-checkout
67+
run: |
68+
NCVERSION='${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}'
69+
wget --quiet "https://download.nextcloud.com/server/releases/latest-$NCVERSION.zip"
70+
unzip "latest-$NCVERSION.zip"
71+
72+
- name: Checkout server master fallback
73+
uses: actions/checkout@v4
74+
if: ${{ steps.server-checkout.outcome != 'success' }}
75+
with:
76+
persist-credentials: false
77+
submodules: true
78+
repository: nextcloud/server
79+
path: nextcloud
80+
81+
- name: Sign app
82+
run: |
83+
cd "$APP_ID/build/artifacts"
84+
tar -xvf "$APP_ID.tar.gz"
85+
cd ../../../
86+
echo '${{ secrets.APP_PRIVATE_KEY }}' > "$APP_ID.key"
87+
wget --quiet "https://github.com/nextcloud/app-certificate-requests/raw/master/$APP_ID/$APP_ID.crt"
88+
php nextcloud/occ integrity:sign-app --privateKey="$APP_ID.key" --certificate="$APP_ID.crt" --path="$APP_ID/build/artifacts/$APP_ID"
89+
cd "$APP_ID/build/artifacts"
90+
tar -zcvf "$APP_ID.tar.gz" "$APP_ID"
91+
92+
- name: Attach tarball to github release
93+
uses: svenstaro/upload-release-action@v2
94+
id: attach_to_release
95+
with:
96+
repo_token: ${{ secrets.GITHUB_TOKEN }}
97+
file: ${{ env.APP_ID }}/build/artifacts/${{ env.APP_ID }}.tar.gz
98+
asset_name: ${{ env.APP_ID }}-${{ github.ref_name }}.tar.gz
99+
tag: ${{ github.ref }}
100+
overwrite: true
101+
102+
- name: Upload app to Nextcloud appstore
103+
uses: nextcloud-releases/nextcloud-appstore-push-action@v1.0.3
104+
with:
105+
app_name: ${{ env.APP_ID }}
106+
appstore_token: ${{ secrets.APPSTORE_TOKEN }}
107+
download_url: ${{ steps.attach_to_release.outputs.browser_download_url }}
108+
app_private_key: ${{ secrets.APP_PRIVATE_KEY }}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
name: Publish Recognize ExApp image
5+
6+
on:
7+
push:
8+
tags:
9+
- v*
10+
workflow_dispatch:
11+
12+
permissions:
13+
packages: write
14+
contents: read
15+
16+
jobs:
17+
push_to_registry:
18+
name: Build and push ExApp image
19+
runs-on: ubuntu-22.04
20+
if: ${{ github.repository_owner == 'nextcloud' }}
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
with:
25+
persist-credentials: false
26+
27+
- name: Set up QEMU
28+
uses: docker/setup-qemu-action@v3
29+
30+
- name: Set up Docker Buildx
31+
uses: docker/setup-buildx-action@v3
32+
33+
- name: Log in to GitHub Container Registry
34+
uses: docker/login-action@v3
35+
with:
36+
registry: ghcr.io
37+
username: ${{ github.actor }}
38+
password: ${{ secrets.GITHUB_TOKEN }}
39+
40+
- name: Install xmlstarlet
41+
run: sudo apt-get update && sudo apt-get install -y xmlstarlet
42+
43+
- name: Extract version from manifest
44+
id: extract_version
45+
run: |
46+
VERSION=$(xmlstarlet sel -t -v "//image-tag" exapp/appinfo/info.xml)
47+
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
48+
echo "Extracted version: $VERSION"
49+
50+
- name: Build and push CPU image
51+
uses: docker/build-push-action@v6
52+
with:
53+
push: true
54+
context: ./exapp
55+
platforms: linux/amd64
56+
build-args: |
57+
BUILD_TYPE=cpu
58+
RECOGNIZE_REF=v${{ env.VERSION }}
59+
tags: |
60+
ghcr.io/nextcloud/recognize_exapp:${{ env.VERSION }}
61+
ghcr.io/nextcloud/recognize_exapp:latest

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [Unreleased]
8+
9+
### Added
10+
11+
* feat: Optionally offload classification to an External App (ExApp) backend, so the machine learning inference can run on a different, more powerful machine — optionally with GPU support ([#73](https://github.com/nextcloud/recognize/issues/73)). The "Recognize classification backend" ExApp is installable from the Nextcloud App Store / External Apps page (requires AppAPI). Select the backend under Admin settings → Recognize → Classification backend. The ExApp sources, manifest and release workflows live in `exapp/`.
12+
713
## [12.0.0] - 2026-04-07
814

915
### Breaking changes

appinfo/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
['name' => 'admin#nice', 'url' => '/admin/nice', 'verb' => 'GET'],
3535
['name' => 'admin#ffmpeg', 'url' => '/admin/ffmpeg', 'verb' => 'GET'],
3636
['name' => 'admin#nodejs', 'url' => '/admin/nodejs', 'verb' => 'GET'],
37+
['name' => 'admin#exapp', 'url' => '/admin/exapp', 'verb' => 'GET'],
3738
['name' => 'admin#libtensorflow', 'url' => '/admin/libtensorflow', 'verb' => 'GET'],
3839
['name' => 'admin#wasmtensorflow', 'url' => '/admin/wasmtensorflow', 'verb' => 'GET'],
3940
['name' => 'admin#gputensorflow', 'url' => '/admin/gputensorflow', 'verb' => 'GET'],

exapp/.nextcloudignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
.git
4+
.github
5+
.gitignore
6+
/.nextcloudignore
7+
/build
8+
/Dockerfile
9+
/Makefile
10+
/krankerl.toml
11+
/server.js
12+
/node_modules
13+
/src

exapp/Dockerfile

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
#
4+
# Recognize ExApp classification backend.
5+
#
6+
# This image is self-contained: it fetches the matching recognize sources
7+
# (classifier scripts + assets) at build time, so it can be built straight from
8+
# this directory (`context: ./exapp`) as the Nextcloud App Store / CI expects.
9+
#
10+
# BUILD_TYPE selects the Tensorflow backend:
11+
# cpu (default) → node:20 base, @tensorflow/tfjs-node
12+
# gpu → CUDA base image, @tensorflow/tfjs-node-gpu
13+
#
14+
# docker build -t recognize_exapp:latest .
15+
# docker build --build-arg BUILD_TYPE=gpu -t recognize_exapp:latest-gpu .
16+
17+
ARG BUILD_TYPE=cpu
18+
19+
###############################################################################
20+
# Base images
21+
###############################################################################
22+
FROM node:20-bookworm-slim AS base-cpu
23+
ENV RECOGNIZE_GPU=false
24+
# Build tools for native node modules (@tensorflow/tfjs-node postinstall).
25+
RUN apt-get update \
26+
&& apt-get install -y --no-install-recommends python3 make g++ \
27+
&& rm -rf /var/lib/apt/lists/*
28+
29+
FROM nvidia/cuda:12.2.2-cudnn8-devel-ubuntu22.04 AS base-gpu
30+
ENV RECOGNIZE_GPU=true
31+
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
32+
RUN apt-get update \
33+
&& apt-get install -y --no-install-recommends curl ca-certificates gnupg python3 make g++ \
34+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
35+
&& apt-get install -y --no-install-recommends nodejs \
36+
&& rm -rf /var/lib/apt/lists/*
37+
38+
###############################################################################
39+
# Final image
40+
###############################################################################
41+
# hadolint ignore=DL3006
42+
FROM base-${BUILD_TYPE}
43+
44+
# The recognize git ref to pull the classifier sources from. Defaults to the
45+
# tag matching this ExApp's version; CI overrides it to the exact release tag.
46+
ARG RECOGNIZE_REF=v12.1.0
47+
48+
ENV APP_HOST=0.0.0.0
49+
ENV APP_PORT=9000
50+
ENV RECOGNIZE_SRC_DIR=/app/src
51+
52+
WORKDIR /app
53+
54+
RUN apt-get update \
55+
&& apt-get install -y --no-install-recommends curl ca-certificates tar \
56+
&& rm -rf /var/lib/apt/lists/*
57+
58+
# Fetch the recognize sources (src/ + package.json) at the pinned ref.
59+
RUN curl -fsSL "https://github.com/nextcloud/recognize/archive/${RECOGNIZE_REF}.tar.gz" -o recognize.tar.gz \
60+
&& tar -xzf recognize.tar.gz --strip-components=1 --wildcards \
61+
"*/src" "*/package.json" "*/package-lock.json" \
62+
&& rm recognize.tar.gz
63+
64+
# Install the recognize node dependencies (Tensorflow.js etc.).
65+
RUN npm ci --omit=dev
66+
67+
# The ExApp HTTP server.
68+
COPY server.js ./server.js
69+
70+
# Pre-download the models so the container is ready to classify on first request.
71+
# (Comment out to download lazily on the AppAPI /init hook instead.)
72+
RUN node -e "require('./src/model-manager.js').downloadAll()" || echo "Model download deferred to runtime"
73+
74+
EXPOSE 9000
75+
76+
CMD ["node", "server.js"]

exapp/Makefile

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
.DEFAULT_GOAL := help
4+
5+
APP_ID := recognize_exapp
6+
APP_NAME := Recognize classification backend
7+
APP_VERSION := $$(xmlstarlet sel -t -v "//version" appinfo/info.xml)
8+
APP_PORT := 9000
9+
JSON_INFO := "{\"id\":\"$(APP_ID)\",\"name\":\"$(APP_NAME)\",\"daemon_config_name\":\"manual_install\",\"version\":\"$(APP_VERSION)\",\"secret\":\"12345\",\"port\":$(APP_PORT)}"
10+
11+
.PHONY: help
12+
help:
13+
@echo " Welcome to $(APP_NAME) $(APP_VERSION)!"
14+
@echo " "
15+
@echo " Please use \`make <target>\` where <target> is one of"
16+
@echo " "
17+
@echo " build-push builds the CPU image and uploads it to ghcr.io"
18+
@echo " build-push-gpu builds the GPU image and uploads it to ghcr.io"
19+
@echo " "
20+
@echo " > Commands for the nextcloud-docker-dev environment (run on the host):"
21+
@echo " "
22+
@echo " run registers $(APP_NAME) on Nextcloud Latest via the App Store manifest"
23+
@echo " register registers a manually-running $(APP_NAME) into the 'manual_install' daemon"
24+
25+
.PHONY: build-push
26+
build-push:
27+
docker login ghcr.io
28+
docker buildx build --push --platform linux/amd64 \
29+
--build-arg BUILD_TYPE=cpu \
30+
--build-arg RECOGNIZE_REF=v$(APP_VERSION) \
31+
--tag ghcr.io/nextcloud/$(APP_ID):$(APP_VERSION) \
32+
--tag ghcr.io/nextcloud/$(APP_ID):latest .
33+
34+
.PHONY: build-push-gpu
35+
build-push-gpu:
36+
docker login ghcr.io
37+
docker buildx build --push --platform linux/amd64 \
38+
--build-arg BUILD_TYPE=gpu \
39+
--build-arg RECOGNIZE_REF=v$(APP_VERSION) \
40+
--tag ghcr.io/nextcloud/$(APP_ID):$(APP_VERSION)-gpu \
41+
--tag ghcr.io/nextcloud/$(APP_ID):latest-gpu .
42+
43+
.PHONY: run
44+
run:
45+
docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:unregister $(APP_ID) --silent --force || true
46+
docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:register $(APP_ID) \
47+
--info-xml https://raw.githubusercontent.com/nextcloud/recognize/main/exapp/appinfo/info.xml
48+
49+
.PHONY: register
50+
register:
51+
docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:unregister $(APP_ID) --silent --force || true
52+
docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:register $(APP_ID) manual_install --json-info $(JSON_INFO) --wait-finish

0 commit comments

Comments
 (0)