Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f3bc258
Initial plan
Copilot Oct 6, 2025
eb35566
Phase 1 complete: Plugin foundation and AI services infrastructure
Copilot Oct 6, 2025
efb728f
Phase 3A complete: Core plugin functionality with segment management …
Copilot Oct 6, 2025
200424e
Final documentation: API docs, CHANGELOG, and CONTRIBUTING guide
Copilot Oct 6, 2025
0464292
Add comprehensive project summary
Copilot Oct 6, 2025
e85cb77
Add comprehensive implementation tracker for all phases and tasks
Copilot Oct 6, 2025
312712d
Updated gitignore
BarbellDwarf Oct 7, 2025
0245038
updated gitignore to remove docker-compose file
BarbellDwarf Oct 7, 2025
641af56
feat: implement dynamic filtering with raw AI score storage
BarbellDwarf Oct 8, 2025
5f07376
fix: add missing using statements for dynamic filtering
BarbellDwarf Oct 8, 2025
32f2421
docs,ai-services: Update templates and documentation for AI service d…
BarbellDwarf Oct 8, 2025
bd8731e
chore: Add .gitignore and .env.example for environment and file exclu…
BarbellDwarf Oct 8, 2025
4a00f53
feat(scripts): Add download-models.py for automated model setup and v…
BarbellDwarf Oct 8, 2025
9587a6d
chore: Update .gitignore to exclude additional environment and test f…
BarbellDwarf Oct 8, 2025
3aa7303
fix(docker): Silence PyTorch CuBLAS determinism warnings in all AI se…
BarbellDwarf Oct 8, 2025
07b575c
chore: Remove deprecated docker-compose.yml file for AI services
BarbellDwarf Oct 8, 2025
0886027
chore: Remove deprecated docker-compose-test.yml file for AI services
BarbellDwarf Oct 8, 2025
6fa69be
fix: DI registration, runtime filtering, and documentation accuracy
BarbellDwarf May 15, 2026
ddd81ed
feat(ai-services): harden inference layer β€” model manifest, /ready en…
BarbellDwarf May 15, 2026
565672a
Track D: docs rewrite, C# test project, Python service tests, rollout…
BarbellDwarf May 15, 2026
2a1a462
fix: remove mock fallbacks in content-classifier; add checksum to man…
BarbellDwarf May 15, 2026
cfef71a
feat: PureFin rename, net9/10.11.8 upgrade, segments UI, dynamic scen…
BarbellDwarf May 18, 2026
8012772
fix: use MAX aggregation for nudity/immodesty scoring; calibrate per-…
BarbellDwarf May 18, 2026
cb4a7bc
feat: store raw scores for all scenes to allow dynamic filtering
BarbellDwarf May 18, 2026
20ab741
Fix false positive nudity detection with dual-model confirmation gate
BarbellDwarf May 18, 2026
81d5273
fix: raise violence threshold, lower immodesty threshold to reduce fa…
BarbellDwarf May 19, 2026
875573c
Add AMD ROCm support, E2E test script, and all prior session work
BarbellDwarf May 19, 2026
86c2a77
Stage all remaining modified files from session work
BarbellDwarf May 19, 2026
2347349
Fix E2E test script: robust profile cycling and /runtime parsing
BarbellDwarf May 19, 2026
2b5f53e
feat(amd): use rocm/pytorch:latest base image for AMD GPU services
BarbellDwarf May 20, 2026
2a3b5f8
fix(healthcheck): use /ready instead of /health for nsfw-detector and…
BarbellDwarf May 20, 2026
4024262
fix(scene-analyzer): correct FFmpeg hwaccel probe - prefer VAAPI over…
BarbellDwarf May 20, 2026
62c3487
feat(gpu): multi-manufacturer GPU support with per-vendor Dockerfiles…
BarbellDwarf May 20, 2026
b4a3de9
feat: migrate nsfw-detector to PyTorch for full GPU acceleration
BarbellDwarf May 20, 2026
327c09d
feat: CLIP 3-class prompts, AMD GPU ROCm base image, E2E validation
May 20, 2026
392cdda
feat: always score all categories; add profanity-detector service
May 20, 2026
3f4013b
feat: enable openai-whisper in profanity-detector requirements
May 20, 2026
e2326ca
Enhance AI services with Profanity Detector and model updates
May 21, 2026
d4d7aa7
feat: Add auto-pause feature during transcoding and enhance segment m…
May 21, 2026
ae84d28
Add admin segment edit entry and selective AI release artifacts
May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions .github/scripts/generate_manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Generate/update Jellyfin plugin repository manifest."""

import argparse
import json
import os
import sys
from datetime import datetime, timezone

def load_build_yaml(path="build.yaml"):
"""Load plugin metadata from build.yaml."""
import re
with open(path) as f:
content = f.read()

def get_field(name):
match = re.search(rf'^{name}:\s*["\']?([^"\'\n]+)["\']?', content, re.MULTILINE)
return match.group(1).strip() if match else ""

return {
"guid": get_field("guid"),
"name": get_field("name"),
"description": get_field("description") or get_field("overview"),
"overview": get_field("overview") or get_field("description"),
"owner": get_field("owner"),
"category": get_field("category"),
"targetAbi": get_field("targetAbi"),
"imageUrl": get_field("imageUrl"),
}


def generate_manifest(version, tag, repo, output, build_yaml="build.yaml", checksum=""):
meta = load_build_yaml(build_yaml)

zip_name = f"{meta['name'].replace(' ', '_')}_{version}.zip"
source_url = f"https://github.com/{repo}/releases/download/{tag}/{zip_name}"

# If checksum not provided, try to read from a .md5 file beside the zip
if not checksum:
md5_path = f"{zip_name}.md5"
if os.path.exists(md5_path):
with open(md5_path) as f:
checksum = f.read().strip()

new_version_entry = {
"version": version,
"changelog": f"Release {tag}. See https://github.com/{repo}/releases/tag/{tag}",
"targetAbi": meta.get("targetAbi", "10.9.0.0"),
"sourceUrl": source_url,
"checksum": checksum,
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
}

# Load existing manifest or create new
manifest = []
if os.path.exists(output):
with open(output) as f:
try:
manifest = json.load(f)
except json.JSONDecodeError:
manifest = []

# Find or create plugin entry
plugin_entry = None
for entry in manifest:
if entry.get("guid") == meta["guid"]:
plugin_entry = entry
break

if plugin_entry is None:
plugin_entry = {
"guid": meta["guid"],
"name": meta["name"],
"description": meta.get("description", ""),
"overview": meta.get("overview", ""),
"owner": meta.get("owner", ""),
"category": meta.get("category", "General"),
"imageUrl": meta.get("imageUrl", ""),
"versions": []
}
manifest.append(plugin_entry)

# Prepend new version (newest first)
versions = plugin_entry.get("versions", [])
versions = [v for v in versions if v["version"] != version] # remove existing same-version entry
versions.insert(0, new_version_entry)
plugin_entry["versions"] = versions

os.makedirs(os.path.dirname(output) if os.path.dirname(output) else ".", exist_ok=True)
with open(output, "w") as f:
json.dump(manifest, f, indent=2)

print(f"Manifest written to {output}")
print(f"Plugin: {meta['name']} v{version}")
print(f"sourceUrl: {source_url}")


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--version", required=True)
parser.add_argument("--tag", required=True)
parser.add_argument("--repo", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--build-yaml", default="build.yaml")
parser.add_argument("--checksum", default="")
args = parser.parse_args()
generate_manifest(args.version, args.tag, args.repo, args.output, args.build_yaml, args.checksum)
42 changes: 42 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Build

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore --configuration Release

- name: Run tests
run: dotnet test --no-build --configuration Release --logger "trx;LogFileName=test-results.trx"

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: '**/*.trx'
retention-days: 7

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: plugin-build
path: '**/bin/Release/net9.0/'
retention-days: 7
24 changes: 24 additions & 0 deletions .github/workflows/init-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Initialize gh-pages

on:
workflow_dispatch:

jobs:
init:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Create gh-pages branch
run: |
git clone https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} repo
cd repo
git checkout --orphan gh-pages
git rm -rf .
echo '[]' > repository.json
echo '<h1>PureFin Plugin Repository</h1><p>Add this URL to Jellyfin: <code>https://BarbellDwarf.github.io/PureFin-Plugin/repository.json</code></p>' > index.html
git add repository.json index.html
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -m "Initialize gh-pages"
git push origin gh-pages
191 changes: 191 additions & 0 deletions .github/workflows/release-ai-services.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
name: Release AI Services

on:
push:
branches: [main]
paths:
- 'ai-services/**'
- '.github/workflows/release-ai-services.yml'

permissions:
contents: write

jobs:
release-ai-services:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Detect releasable AI service changes
id: detect
shell: bash
run: |
set -euo pipefail

BEFORE_SHA="${{ github.event.before }}"
if [[ -z "${BEFORE_SHA}" || "${BEFORE_SHA}" == "0000000000000000000000000000000000000000" ]]; then
CHANGED_FILES="$(git diff-tree --no-commit-id --name-only -r HEAD || true)"
else
CHANGED_FILES="$(git diff --name-only "${BEFORE_SHA}" "${GITHUB_SHA}" || true)"
fi

declare -A CHANGED_SERVICES=()
SHARED_CHANGED=false

while IFS= read -r file; do
[[ -z "${file}" ]] && continue

if [[ "${file}" =~ ^ai-services/services/([^/]+)/ ]]; then
CHANGED_SERVICES["${BASH_REMATCH[1]}"]=1
continue
fi

if [[ "${file}" =~ ^ai-services/(docker-compose.*\.yml|\.env\.example|README\.md|SETUP\.md|GPU_SETUP\.md|DEPLOYMENT_OPTIONS\.md|PATH_CONFIGURATION\.md|scripts/|models/) ]]; then
SHARED_CHANGED=true
fi
done <<< "${CHANGED_FILES}"

SERVICES=""
if [[ "${SHARED_CHANGED}" == "true" ]]; then
for service_dir in ai-services/services/*; do
service_name="$(basename "${service_dir}")"
SERVICES="${SERVICES} ${service_name}"
done
else
for service_name in "${!CHANGED_SERVICES[@]}"; do
SERVICES="${SERVICES} ${service_name}"
done
fi

SERVICES="$(echo "${SERVICES}" | xargs || true)"
if [[ -z "${SERVICES}" ]]; then
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "services=" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "services=${SERVICES}" >> "$GITHUB_OUTPUT"

- name: Compute next semver
id: semver
if: steps.detect.outputs.should_release == 'true'
shell: bash
run: |
set -euo pipefail
git fetch --tags --force

LAST_TAG="$(git tag --list 'ai-services-v*' --sort=-v:refname | head -n1 || true)"
if [[ -z "${LAST_TAG}" ]]; then
BASE_VERSION="0.1.0"
else
BASE_VERSION="${LAST_TAG#ai-services-v}"
fi

RANGE_SPEC=""
if [[ -n "${LAST_TAG}" ]]; then
RANGE_SPEC="${LAST_TAG}..HEAD"
fi

COMMITS="$(git log ${RANGE_SPEC} --pretty=format:%s || true)"
BODY="$(git log ${RANGE_SPEC} --pretty=format:%b || true)"

BUMP="patch"
if echo "${COMMITS}"$'\n'"${BODY}" | grep -Eq 'BREAKING CHANGE|!:'; then
BUMP="major"
elif echo "${COMMITS}" | grep -Eq '^feat(\(.+\))?: '; then
BUMP="minor"
fi

IFS='.' read -r MAJOR MINOR PATCH <<< "${BASE_VERSION}"
case "${BUMP}" in
major)
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
minor)
MINOR=$((MINOR + 1))
PATCH=0
;;
*)
PATCH=$((PATCH + 1))
;;
esac

VERSION="${MAJOR}.${MINOR}.${PATCH}"
TAG="ai-services-v${VERSION}"

echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "bump=${BUMP}" >> "$GITHUB_OUTPUT"
echo "last_tag=${LAST_TAG}" >> "$GITHUB_OUTPUT"

- name: Build AI service setup bundles
if: steps.detect.outputs.should_release == 'true'
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.semver.outputs.version }}"
mkdir -p dist/ai-services

IFS=' ' read -r -a SERVICES <<< "${{ steps.detect.outputs.services }}"

for service_name in "${SERVICES[@]}"; do
service_dir="ai-services/services/${service_name}"
staging="dist/staging/${service_name}"
mkdir -p "${staging}/services"

cp -R "${service_dir}" "${staging}/services/${service_name}"
cp ai-services/docker-compose*.yml "${staging}/" || true
cp ai-services/.env.example "${staging}/" || true
cp ai-services/README.md "${staging}/" || true
cp ai-services/SETUP.md "${staging}/" || true
cp ai-services/GPU_SETUP.md "${staging}/" || true
cp ai-services/DEPLOYMENT_OPTIONS.md "${staging}/" || true
cp ai-services/PATH_CONFIGURATION.md "${staging}/" || true
cp -R ai-services/scripts "${staging}/scripts" || true
cp -R ai-services/models "${staging}/models" || true

(cd "${staging}" && zip -r "../../ai-services/${service_name}-${VERSION}.zip" .)
done

zip -r "dist/ai-services/purefin-ai-stack-${VERSION}.zip" ai-services \
-x "*/__pycache__/*" "*.pyc" "*/.pytest_cache/*" "*/.venv/*"

- name: Upload workflow artifacts
if: steps.detect.outputs.should_release == 'true'
uses: actions/upload-artifact@v4
with:
name: ai-services-${{ steps.semver.outputs.version }}
path: dist/ai-services/*.zip
retention-days: 30

- name: Create Git tag
if: steps.detect.outputs.should_release == 'true'
shell: bash
run: |
set -euo pipefail
TAG="${{ steps.semver.outputs.tag }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git rev-parse "${TAG}" >/dev/null 2>&1; then
echo "Tag ${TAG} already exists; skipping."
exit 0
fi
git tag "${TAG}"
git push origin "${TAG}"

- name: Publish GitHub release
if: steps.detect.outputs.should_release == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.semver.outputs.tag }}
name: AI Services ${{ steps.semver.outputs.version }}
target_commitish: ${{ github.sha }}
generate_release_notes: true
files: dist/ai-services/*.zip
Loading
Loading