From 01a5e50fcde199794ffd35c914c7cc403309e191 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 13 Sep 2026 14:38:14 -0400 Subject: [PATCH] AI Scanning --- .env | 4 +- aiworker/Dockerfile | 29 ++++++++ aiworker/docker-compose.yml | 32 ++++++++ aiworker/run.pl | 145 ++++++++++++++++++++++++++++++++++++ api/app.pl | 2 +- run.pl | 68 ----------------- scanner/scanner.pl | 4 +- 7 files changed, 212 insertions(+), 72 deletions(-) create mode 100644 aiworker/Dockerfile create mode 100644 aiworker/docker-compose.yml create mode 100755 aiworker/run.pl delete mode 100755 run.pl diff --git a/.env b/.env index 6906023..deddf63 100644 --- a/.env +++ b/.env @@ -4,8 +4,8 @@ # ----------------------------------------------------- # Database # ----------------------------------------------------- -DB_ROOT_PASSWORD=changeme_root -DB_PASSWORD=changeme_videodetect +DB_ROOT_PASSWORD=videodetect +DB_PASSWORD=videodetect DB_USER=videodetect DB_NAME=videodetect DB_HOST=mariadb diff --git a/aiworker/Dockerfile b/aiworker/Dockerfile new file mode 100644 index 0000000..fc5bfba --- /dev/null +++ b/aiworker/Dockerfile @@ -0,0 +1,29 @@ +FROM ubuntu:22.04 + +# Avoid interactive prompts during build +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=Etc/UTC + +# System dependencies +# Install Perl, system deps, and ffmpeg (includes ffprobe) +RUN apt update && apt install -y \ + libdbd-mysql-perl \ + libhttp-lite-perl \ + libjson-perl \ + libyaml-perl \ + perl \ + libdbi-perl \ + build-essential \ + ffmpeg \ + libstring-shellquote-perl \ + libwww-perl \ + liblwp-protocol-https-perl + + +RUN mkdir -p /app +ADD run.pl /app/run.pl +RUN chmod +x /app/run.pl +WORKDIR /app + +# Default command: process tasks from the API until drained +CMD ["perl", "/app/run.pl"] diff --git a/aiworker/docker-compose.yml b/aiworker/docker-compose.yml new file mode 100644 index 0000000..5857837 --- /dev/null +++ b/aiworker/docker-compose.yml @@ -0,0 +1,32 @@ +# VideoDetect — Worker service (standalone compose file) +# Run: docker compose -f aiworker/docker-compose.yml up -d + +services: + worker: + build: + context: ./ + dockerfile: Dockerfile + container_name: videodetect-aiworker + restart: "no" + environment: + - API_BASE_URL=${API_BASE_URL:-http://videodetect-api:3000} + - OLLAMA_URL=${OLLAMA_URL:-http://10.0.1.7:11434} + - SECONDS_PER_FRAME=${SECONDS_PER_FRAME:-30} + - TASK_COUNT=${TASK_COUNT:-50} + volumes: + - nas_input:/data:ro + - ./run.pl:/app/run.pl:ro + networks: + - videodetect_videodetect-network + +volumes: + nas_input: + driver: local + driver_opts: + type: nfs + o: addr=10.0.0.2,ro,nfsvers=4,hard,intr + device: ":/mnt/Bulk/Homes/ryan/Prawns" + +networks: + videodetect_videodetect-network: + external: true diff --git a/aiworker/run.pl b/aiworker/run.pl new file mode 100755 index 0000000..6c20ace --- /dev/null +++ b/aiworker/run.pl @@ -0,0 +1,145 @@ +#!/usr/bin/perl +use strict; +use MIME::Base64 qw(encode_base64); +use LWP::UserAgent; +use JSON; +use Data::Dumper; + +my $seconds = $ENV{'SECONDS_PER_FRAME'} || 30; +my $ollama = $ENV{'OLLAMA_URL'} || die "OLLAMA_URL environment variable not set\n"; +my $api = $ENV{'API_BASE_URL'} || die "API_BASE_URL environment variable not set\n"; +my $task_count = $ENV{'TASK_COUNT'} || 1; +my $sensitivity = 20; +my $resolution = "672:672"; + +chdir("/tmp") || die "Unable to change directory to /tmp: $!\n"; +my $ua = LWP::UserAgent->new; +$ua->timeout(60); + +for(my $i=0; $i<$task_count; $i++) { + my $ref = GetNext(); + my $task = $ref->{'task'}; + my $video = $ref->{'video'}; + + my $task_id = $task->{'id'}; + my $video_id = $video->{'id'}; + my $path = $video->{'file_path'}; + my $result = CheckPath($path); + + my $response = PostResponse($task_id, $ref->{'assign_key'}, $result); +} + +print "Done\n"; +exit(0); + +sub CheckPath { + my ($path) = @_; + die "File path not set\n" unless $path; + die "File does not exist: $path\n" unless -e $path; + + unlink(<*.png>); + system("ffmpeg -i \"$path\" -vf fps=1/$seconds,scale=$resolution output_%04d.png"); + + + my $total=0; + my $yes=0; + $/ = undef; + foreach my $file (<*.png>) { + print "$file...\n"; + open(IN, '<:raw', $file) || die "Unable to read $file: $!\n"; + my $data = ; + close(IN); + + my $base64 = encode_base64($data); + $data=undef; + + my $ref = { + 'model' => 'srizon/pixie:latest', + 'prompt' => "Are there any african american people or in this photo, whether they are shown completely or not? Please answer 'yes' or 'no' only.", + 'images' => [$base64], + }; + + my $url = "$ollama/api/generate"; + my $res = GetRest($url, $ref); + print "\t$res\n"; + $total++; + if($res=~/yes/i) { + $yes++; + } + unlink($file); + } + + my $percentage = ($yes*100)/$total; + print "$yes of $total images gave yes - $percentage\n"; + my $verdict = 0; + if($percentage >= $sensitivity) { + print "Probably a black dude, $percentage\n"; + $verdict = 1; + } else { + print "Probably not, $percentage\n"; + $verdict = 0; + } + return { + 'images' => $total, + 'yes' => $yes, + 'percentage'=> $percentage, + 'verdict' => $verdict, + + }; +} + +sub GetRest { + my($url, $ref)=@_; + my $payload = encode_json($ref); + + my $response = $ua->post($url, 'Content_Type' => 'application/json', 'Content' => $payload); + if($response->is_success) { + my $content = $response->decoded_content; + my @parts = split(/\n/, $content); + my @refs = map { decode_json($_) } @parts; + return join(' ', map { $_->{'response'} } @refs); + } else { + die "Error: " . $response->status_line; + } +} + +sub PostResponse { + my ($task_id, $assign_key, $results) = @_; + my $url = "$api/api/v1/task/$task_id/complete"; + my $ref = { + 'assign_key' => $assign_key, + 'results' => $results, + }; + my $payload = encode_json($ref); + my $response = $ua->post($url, 'Content_Type' => 'application/json', 'Content' => $payload); + if($response->is_success) { + return $response->decoded_content; + } else { + die "Error posting response ($url): " . $response->status_line; + } +} + +sub GetNext { + my $url = "$api/api/v1/nexttask/AISCAN"; + my $task; + my $video; + + my $response = $ua->get($url); + if($response->is_success) { + $task=decode_json($response->decoded_content); + } else { + die "Error fetching next task ($url): " . $response->status_line; + } + + my $video_id = $task->{'task'}{'video_id'}; + $url = "$api/api/v1/video/$video_id"; + $response = $ua->get($url); + if($response->is_success) { + $video=decode_json($response->decoded_content); + $task->{'video'} = $video; + } else { + die "Error fetching video ($url): " . $response->status_line; + } + + return $task; +} \ No newline at end of file diff --git a/api/app.pl b/api/app.pl index 67403fc..7ee49ac 100644 --- a/api/app.pl +++ b/api/app.pl @@ -107,7 +107,7 @@ get '/api/v1/task/:task' => sub { get '/api/v1/nexttask/:type' => sub { my $type = route_parameters->get("type"); - my $sth = database->prepare("SELECT * FROM tasks WHERE task_type=? AND status='PENDING' ORDER BY id LIMIT 1"); + my $sth = database->prepare("SELECT * FROM tasks WHERE task_type=? AND status='PENDING' ORDER BY updated_at ASC LIMIT 1"); $sth->execute($type); my $ref = $sth->fetchrow_hashref(); $sth->finish(); diff --git a/run.pl b/run.pl deleted file mode 100755 index c62f5c4..0000000 --- a/run.pl +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/perl -use strict; -use MIME::Base64 qw(encode_base64); -use LWP::UserAgent; -use JSON; -use Data::Dumper; - -my $seconds = 30; -my $ollama = 'http://10.0.1.7:11434'; -my $resolution = "672:672"; -my $sensitivity = 20; - -my $file = $ARGV[0] || die "Usage: $0 \n"; - -system("ffmpeg -i \"$file\" -vf fps=1/$seconds,scale=$resolution output_%04d.png"); -my $ua = LWP::UserAgent->new; -$ua->timeout(60); - -my $total=0; -my $yes=0; -$/ = undef; -foreach my $file (<*.png>) { - print "$file...\n"; - open(IN, '<:raw', $file) || die "Unable to read $file: $!\n"; - my $data = ; - close(IN); - - my $base64 = encode_base64($data); - $data=undef; - - my $ref = { - 'model' => 'srizon/pixie:latest', - 'prompt' => "Are there any african american people or in this photo, whether they are shown completely or not? Please answer 'yes' or 'no' only.", - 'images' => [$base64], - }; - - my $url = "$ollama/api/generate"; - my $res = GetRest($url, $ref); - print "\t$res\n"; - $total++; - if($res=~/yes/i) { - $yes++; - } - unlink($file); -} - -my $percentage = ($yes*100)/$total; -print "$yes of $total images gave yes - $percentage\n"; -if($percentage >= $sensitivity) { - print "Probably a black dude, $percentage\n"; -} else { - print "Probably not, $percentage\n"; -} - -sub GetRest { - my($url, $ref)=@_; - my $payload = encode_json($ref); - - my $response = $ua->post($url, 'Content_Type' => 'application/json', 'Content' => $payload); - if($response->is_success) { - my $content = $response->decoded_content; - my @parts = split(/\n/, $content); - my @refs = map { decode_json($_) } @parts; - return join(' ', map { $_->{'response'} } @refs); - } else { - die "Error: " . $response->status_line; - } -} diff --git a/scanner/scanner.pl b/scanner/scanner.pl index 3f04063..65c9157 100755 --- a/scanner/scanner.pl +++ b/scanner/scanner.pl @@ -17,6 +17,7 @@ $SIG{INT} = sub { die "Interrupted\n"; }; my $dbh = DBI->connect("DBI:mysql:database=$db_name;host=$db_host", $db_user, $db_pass); my @video_extensions = qw(mp4 mkv avi mov flv wmv mpg mpeg webm); +my %ignored_extensions; my @dir_queue = ('/data'); @@ -45,12 +46,13 @@ while(my $dir = shift @dir_queue) { # Any entry where last_scan_time < $now is considered deleted or moved, so we can mark them as such my $sth_delete = $dbh->prepare("DELETE FROM videos WHERE last_scan_time < ? OR last_scan_time IS NULL"); $sth_delete->execute($now); +print "Ignored extensions: " . join(", ", keys %ignored_extensions) . "\n"; sub process_file { my ($file_path) = @_; - # Determine if the file is a video based on mime info or extension. For simplicity, let's check the extension. if (my ($ext)=$file_path =~ /^.+\.(\S+?)$/) { unless (grep { lc($ext) eq $_ } @video_extensions) { + $ignored_extensions{lc($ext)} = 1; return; # Not a video file } }