├── .styleci.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── config.php ├── database └── migrations │ └── create_media_recognitions_table.php.stub ├── phpunit.xml └── src ├── Contracts └── MediaRecognition.php ├── Events ├── FacialAnalysisCompleted.php ├── LabelAnalysisCompleted.php ├── ModerationAnalysisCompleted.php └── TextAnalysisCompleted.php ├── Facades └── Recognize.php ├── Http ├── Controllers │ └── IncomingWebhookController.php └── Middleware │ └── VerifySignature.php ├── Jobs ├── StartFaceDetection.php ├── StartLabelDetection.php ├── StartModerationDetection.php └── StartTextDetection.php ├── MediaRecognitionManager.php ├── Models └── MediaRecognition.php ├── Providers └── MediaRecognitionServiceProvider.php ├── Recognizers └── Rekognition.php ├── Traits ├── CanRecognizeImages.php ├── CanRecognizeVideos.php ├── InteractsWithStorage.php └── Recognizable.php └── routes.php /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-media-recognition` will be documented in this file. 4 | 5 | ## 1.0.1 - 2021-03-25 6 | 7 | - fix: video & image response structure is different for faces & face details 8 | 9 | ## 1.0.0 - 2021-03-25 10 | 11 | - initial release 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) Meema, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Media Recognition Package for Laravel 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/meema/laravel-media-recognition.svg?style=flat)](https://packagist.org/packages/meema/laravel-media-recognition) 4 | [![GitHub Workflow Status](https://github.com/meemalabs/laravel-media-recognition/actions/workflows/run-tests.yml/badge.svg?label=tests)](https://github.com/meemalabs/laravel-media-recognition) 5 | [![StyleCI](https://github.styleci.io/repos/227280228/shield?branch=main&style=flat)](https://github.styleci.io/repos/227280228) 6 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/meemalabs/laravel-media-recognition/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/meemalabs/laravel-media-recognition/?branch=main) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/meema/laravel-media-recognition.svg?style=flat)](https://packagist.org/packages/meema/laravel-media-recognition) 8 | [![Discord](https://img.shields.io/discord/834503516134441012?label=discord&dstyle=flat)](https://discord.meema.io) 9 | [![License](https://img.shields.io/github/license/meemalabs/laravel-media-recognition.svg?style=flat)](https://github.com/meemalabs/laravel-media-recognition/blob/main/LICENSE.md) 10 | 11 | At the current state, this is a wrapper package for AWS Rekognition with some extra handy methods. 12 | 13 | ![laravel-media-recognition package image](https://banners.beyondco.de/Media%20Recognition.png?theme=light&packageManager=composer+require&packageName=meema%2Flaravel-media-recognition&pattern=architect&style=style_1&description=Easily+%26+quickly+recognize+the+content+of+your+images+%26+video+content&md=1&showWatermark=1&fontSize=100px&images=https%3A%2F%2Flaravel.com%2Fimg%2Flogomark.min.svg) 14 | 15 | ## 💡 Usage 16 | 17 | ``` php 18 | use Meema\MediaRecognition\Facades\Recognize; 19 | 20 | // run any of the following methods: 21 | // note: any of the detect*() method parameters are optional and will default to config values 22 | 23 | // "image operations" 24 | $recognize = Recognize::path('images/persons.jpg', 'image/jpeg'); // while the $mimeType parameter is optional, it is recommended for performance reasons 25 | $recognize->detectLabels($minConfidence = null, $maxLabels = null) 26 | $recognize->detectFaces($attributes = ['DEFAULT']) 27 | $recognize->detectModeration($minConfidence = null) 28 | $recognize->detectText() 29 | 30 | // "video operations" 31 | $recognize = Recognize::path('videos/amazing-video.mp4', 'video/mp4'); 32 | $recognize->startLabelDetection($minConfidence = null, $maxResults = 1000) 33 | $recognize->startFaceDetection(string $faceAttribute = 'DEFAULT') 34 | $recognize->startContentModeration(int $minConfidence = null) 35 | $recognize->startTextDetection(array $filters = null) 36 | 37 | // get the analysis/status of your jobs 38 | $recognize->getLabelsByJobId(string $jobId) 39 | $recognize->getFacesByJobId(string $jobId) 40 | $recognize->getContentModerationByJobId(string $jobId) 41 | $recognize->getTextDetectionByJobId(string $jobId) 42 | 43 | // if you want to track your media recognitions, use the Recognizable trait on your media model && run the included migration 44 | $media = Media::first(); 45 | $media->recognize($path)->detectFaces(); // you may chain any of the detection methods 46 | ``` 47 | 48 | ## 🐙 Installation 49 | 50 | You can install the package via composer: 51 | 52 | ```bash 53 | composer require meema/laravel-media-recognition 54 | ``` 55 | 56 | The package will automatically register itself. 57 | 58 | Next, publish the config file with: 59 | 60 | ```bash 61 | php artisan vendor:publish --provider="Meema\MediaRecognition\Providers\MediaRecognitionServiceProvider" --tag="config" 62 | ``` 63 | 64 | Next, please add the following keys their values to your `.env` file. 65 | 66 | ```bash 67 | AWS_ACCESS_KEY_ID=xxxxxxx 68 | AWS_SECRET_ACCESS_KEY=xxxxxxx 69 | AWS_DEFAULT_REGION=us-east-1 70 | AWS_SNS_TOPIC_ARN=arn:aws:sns:us-east-1:000000000000:RekognitionUpdate 71 | AWS_S3_BUCKET=bucket-name 72 | ``` 73 | 74 | The following is the content of the published config file: 75 | 76 | ```php 77 | return [ 78 | /** 79 | * The fully qualified class name of the "media" model. 80 | */ 81 | 'media_model' => \App\Models\Media::class, 82 | 83 | /** 84 | * IAM Credentials from AWS. 85 | */ 86 | 'credentials' => [ 87 | 'key' => env('AWS_ACCESS_KEY_ID'), 88 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 89 | ], 90 | 91 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 92 | 93 | /** 94 | * Specify the version of the Rekognition API you would like to use. 95 | * Please only adjust this value if you know what you are doing. 96 | */ 97 | 'version' => 'latest', 98 | 99 | /** 100 | * The S3 bucket name where the image/video to be analyzed is stored. 101 | */ 102 | 'bucket' => env('AWS_S3_BUCKET'), 103 | 104 | /** 105 | * Specify the IAM Role ARN. 106 | * 107 | * You can find the Role ARN visiting the following URL: 108 | * https://console.aws.amazon.com/iam/home?region=us-east-1#/roles 109 | * Please note to adjust the "region" in the URL above. 110 | * Tip: in case you need to create a new Role, you may name it `Rekognition_Default_Role` 111 | * by making use of this name, AWS Rekognition will default to using this IAM Role. 112 | */ 113 | 'iam_arn' => env('AWS_IAM_ARN'), 114 | 115 | /** 116 | * Specify the AWS SNS Topic ARN. 117 | * This triggers the webhook to be sent. 118 | * 119 | * It can be found by selecting your "Topic" when visiting the following URL: 120 | * https://console.aws.amazon.com/sns/v3/home?region=us-east-1#/topics 121 | * Please note to adjust the "region" in the URL above. 122 | */ 123 | 'sns_topic_arn' => env('AWS_SNS_TOPIC_ARN'), 124 | 125 | ]; 126 | ``` 127 | 128 | ## Preparing Your Media Model (optional) 129 | 130 | This package includes a trait for your "Media model" that you may use to define the relationship of your media model with the tracked recognitions. 131 | 132 | Simply use it as follows: 133 | 134 | ```php 135 | namespace App\Models; 136 | 137 | use Illuminate\Database\Eloquent\Model; 138 | use Meema\MediaRecognition\Traits\Recognizable; 139 | 140 | class Media extends Model 141 | { 142 | use Recognizable; 143 | 144 | // ... 145 | } 146 | ``` 147 | 148 | ### Set Up Webhooks (optional) 149 | 150 | This package makes use of webhooks in order to communicate the updates of the AWS Rekognition job. Please follow the following steps to enable webhooks for yourself. 151 | 152 | Please note, this is only optional, and you should only enable this if you want to track the Rekognition job's results for long-lasting processes (e.g. analyzing video). 153 | 154 | #### Setup Expose 155 | 156 | First, let's use [Expose](https://beyondco.de/docs/expose/getting-started/installation) to "expose" / generate a URL for our local API. Follow the Expose documentation on how you can get started and generate a "live" & sharable URL for within your development environment. 157 | 158 | It should be as simple as `cd my-laravel-api && expose`. 159 | 160 | #### Setup AWS SNS Topic & Subscription 161 | 162 | Second, let's create an AWS SNS Topic which will notify our "exposed" API endpoint: 163 | 164 | 1. Open the Amazon SNS console at https://console.aws.amazon.com/sns/v3/home 165 | 2. In the navigation pane, choose Topics, and then choose "Create new topic". 166 | 3. For Topic name, enter `RekognitionUpdate`, and then choose "Create topic". 167 | 168 | ![AWS SNS Topic Creation Screenshot](https://i.imgur.com/4MKtfuY.png) 169 | 170 | 4. Copy & note down the topic ARN which you just created. It should look similar to this: `arn:aws:sns:region:123456789012:RekognitionUpdate`. 171 | 5. On the Topic details: `RekognitionUpdate` page, in the Subscriptions section, choose "Create subscription". 172 | 6. For Protocol, choose "HTTPS". For Endpoint, enter exposed API URL that you generated in a previous step, including the API URI. 173 | 174 | For example, 175 | ``` 176 | https://meema-api.sharedwithexpose.com/api/webhooks/media-recognition 177 | ``` 178 | 179 | 7. Choose "Create subscription". 180 | 181 | #### Confirming Your Subscription 182 | 183 | Finally, we need to ensure the subscription is confirmed. By navigating to the `RekognitionUpdate` Topic page, you should see the following section: 184 | 185 | ![AWS SNS Subscription Confirmation Screenshot](https://i.imgur.com/oTPwNen.png) 186 | 187 | By default, AWS will have sent a post request to URL you defined in your "Subscription" setup. This package automatically handles the "confirmation" part. In case there is an issue and it is not confirmed yet, please click on the "Request confirmation" button as seen in the screenshot above. 188 | 189 | You can also view the request in the Expose interface, by visiting the "Dashboard Url", which should be similar to: `http://127.0.0.1:4040` 190 | 191 | Once the status reflects "Confirmed", your API will receive webhooks as AWS provides updates. 192 | 193 | ## Deploying to Laravel Vapor 194 | 195 | Please note, as of right now, you cannot reuse the AWS credentials stored in your "environment file". The "workaround" for this is to adjust the `media-recognition.php`-config file by renaming 196 | 197 | From: `AWS_ACCESS_KEY_ID` - To: e.g. `VAPOR_ACCESS_KEY_ID` 198 | 199 | From: `AWS_SECRET_ACCESS_KEY` - To: e.g. `VAPOR_SECRET_ACCESS_KEY` 200 | 201 | and, lastly, please ensure that your Vapor environment has these values defined. 202 | 203 | ## 🧪 Testing 204 | 205 | ``` bash 206 | composer test 207 | ``` 208 | 209 | ## 📈 Changelog 210 | 211 | Please see our [releases](https://github.com/meemalabs/laravel-media-recognition/releases) page for more information on what has changed recently. 212 | 213 | ## 💪🏼 Contributing 214 | 215 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 216 | 217 | ## 🏝 Community 218 | 219 | For help, discussion about best practices, or any other conversation that would benefit from being searchable: 220 | 221 | [Media Recognition on GitHub](https://github.com/meemalabs/laravel-media-recognition/discussions) 222 | 223 | For casual chit-chat with others using this package: 224 | 225 | [Join the Meema Discord Server](https://discord.meema.io) 226 | 227 | ## 🚨 Security 228 | 229 | Please review [our security policy](https://github.com/meemalabs/laravel-media-recognition/security/policy) on how to report security vulnerabilities. 230 | 231 | ## 🙏🏼 Credits 232 | 233 | - [Chris Breuer](https://github.com/Chris1904) 234 | - [Folks at Meema](https://github.com/meemalabs) 235 | - [All Contributors](../../contributors) 236 | 237 | ## 📄 License 238 | 239 | The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information. 240 | 241 | Made with ❤️ by Meema, Inc. 242 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meema/laravel-media-recognition", 3 | "description": "Easily & quickly integrate your application with AWS Rekognition. Other drivers may be added in the near future.", 4 | "keywords": [ 5 | "rekognition", 6 | "recognition", 7 | "image analysis", 8 | "video analysis", 9 | "meema", 10 | "laravel", 11 | "aws" 12 | ], 13 | "homepage": "https://github.com/meemalabs/laravel-media-recognition", 14 | "license": "MIT", 15 | "type": "library", 16 | "authors": [ 17 | { 18 | "name": "Chris Breuer", 19 | "email": "chris@meema.io" 20 | } 21 | ], 22 | "require": { 23 | "php": "^7.3|^8.0", 24 | "ext-json": "*", 25 | "aws/aws-php-sns-message-validator": "^1.6", 26 | "aws/aws-sdk-php": "^3.163" 27 | }, 28 | "require-dev": { 29 | "orchestra/testbench": "^3.5.0|^3.6.0|^4.0|^5.0|^6.0", 30 | "pestphp/pest": "^1.0", 31 | "phpunit/phpunit": "^5.0|^6.0|^8.0|^9.3", 32 | "vlucas/phpdotenv": "^4.2|^5.3" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Meema\\MediaRecognition\\": "src" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Meema\\MediaRecognition\\Tests\\": "tests" 42 | } 43 | }, 44 | "scripts": { 45 | "test": "vendor/bin/pest" 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | }, 50 | "extra": { 51 | "laravel": { 52 | "providers": [ 53 | "Meema\\MediaRecognition\\Providers\\MediaRecognitionServiceProvider" 54 | ], 55 | "aliases": { 56 | "MediaRecognition": "Meema\\MediaRecognition\\Facades\\Recognize" 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | \App\Models\Media::class, 8 | 9 | /** 10 | * IAM Credentials from AWS. 11 | * 12 | * Please note, if you are intending to use Laravel Vapor, rename 13 | * From: AWS_ACCESS_KEY_ID - To: e.g. VAPOR_ACCESS_KEY_ID 14 | * From: AWS_SECRET_ACCESS_KEY - To: e.g. VAPOR_SECRET_ACCESS_KEY 15 | * and ensure that your Vapor environment has these values defined. 16 | */ 17 | 'credentials' => [ 18 | 'key' => env('AWS_ACCESS_KEY_ID'), 19 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 20 | ], 21 | 22 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 23 | 24 | /** 25 | * Specify the version of the Rekognition API you would like to use. 26 | * Please only adjust this value if you know what you are doing. 27 | */ 28 | 'version' => 'latest', 29 | 30 | /* 31 | * The disk that your images/videos to analyze are stored on. 32 | * Choose one of the disks you've configured in config/filesystems.php. 33 | */ 34 | 'disk' => env('REKOGNITION_DISK', 's3'), 35 | 36 | /** 37 | * Specify the IAM Role ARN. 38 | * 39 | * You can find the Role ARN visiting the following URL: 40 | * https://console.aws.amazon.com/iam/home?region=us-east-1#/roles 41 | * Please note to adjust the "region" in the URL above. 42 | * Tip: in case you need to create a new Role, you may name it `Rekognition_Default_Role` 43 | * by making use of this name, AWS Rekognition will default to using this IAM Role. 44 | */ 45 | 'iam_arn' => env('AWS_IAM_ARN'), 46 | 47 | /** 48 | * Specify the AWS SNS Topic ARN. 49 | * This triggers the webhook to be sent. 50 | * 51 | * It can be found by selecting your "Topic" when visiting the following URL: 52 | * https://console.aws.amazon.com/sns/v3/home?region=us-east-1#/topics 53 | * Please note to adjust the "region" in the URL above. 54 | */ 55 | 'sns_topic_arn' => env('AWS_SNS_TOPIC_ARN'), 56 | 57 | /** 58 | * Specifies the minimum confidence level for the labels to return. 59 | * Amazon Rekognition doesn't return any labels with confidence lower than this specified value. 60 | * 61 | * If min_confidence is not specified, the operation returns labels with a confidence 62 | * values greater than or equal to 55 percent. 63 | * 64 | * Type: Float 65 | * Valid Range: Minimum value of 0. Maximum value of 100. 66 | */ 67 | 'min_confidence' => 55, 68 | 69 | ]; 70 | -------------------------------------------------------------------------------- /database/migrations/create_media_recognitions_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->morphs('model'); 19 | $table->string('labels_job_id')->nullable(); 20 | $table->string('faces_job_id')->nullable(); 21 | $table->string('moderation_job_id')->nullable(); 22 | $table->string('ocr_job_id')->nullable(); 23 | $table->json('labels')->nullable(); 24 | $table->json('faces')->nullable(); 25 | $table->json('moderation')->nullable(); 26 | $table->json('ocr')->nullable(); 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('media_recognitions'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | ./app 15 | ./src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Contracts/MediaRecognition.php: -------------------------------------------------------------------------------- 1 | message = $message; 26 | $this->mediaId = $mediaId; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Events/LabelAnalysisCompleted.php: -------------------------------------------------------------------------------- 1 | message = $message; 26 | $this->mediaId = $mediaId; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Events/ModerationAnalysisCompleted.php: -------------------------------------------------------------------------------- 1 | message = $message; 26 | $this->mediaId = $mediaId; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Events/TextAnalysisCompleted.php: -------------------------------------------------------------------------------- 1 | message = $message; 26 | $this->mediaId = $mediaId; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Facades/Recognize.php: -------------------------------------------------------------------------------- 1 | middleware('verify-signature'); 20 | } 21 | 22 | /** 23 | * @throws \Exception 24 | */ 25 | public function __invoke() 26 | { 27 | $message = $this->ensureSubscriptionIsConfirmed(); 28 | 29 | Log::info('incoming rekognition webhook message', $message); 30 | 31 | if (! array_key_exists('Status', $message)) { 32 | Log::alert('incoming rekognition webhook: "Status"-key does not exist'); 33 | 34 | return; 35 | } 36 | 37 | if ($message['Status'] !== 'SUCCEEDED') { 38 | Log::alert('incoming rekognition webhook: "Status"-field value does not equal "SUCCEEDED"'); 39 | 40 | return; 41 | } 42 | 43 | $arr = explode('_', $message['JobTag']); 44 | $type = $arr[0]; 45 | $mediaId = (int) $arr[1]; 46 | 47 | try { 48 | $this->fireEventFor($type, $message, $mediaId); 49 | } catch (\Exception $e) { 50 | throw new \Exception($e); 51 | } 52 | } 53 | 54 | /** 55 | * @param string $type 56 | * @param array $message 57 | * @param int|null $mediaId 58 | * @throws \Exception 59 | */ 60 | public function fireEventFor(string $type, array $message, int $mediaId = null) 61 | { 62 | switch ($type) { 63 | case 'labels': 64 | Recognize::getLabelsByJobId($message['JobId'], $mediaId); 65 | event(new LabelAnalysisCompleted($message, $mediaId)); 66 | break; 67 | case 'faces': 68 | Recognize::getFacesByJobId($message['JobId'], $mediaId); 69 | event(new FacialAnalysisCompleted($message, $mediaId)); 70 | break; 71 | case 'moderation': 72 | Recognize::getContentModerationByJobId($message['JobId'], $mediaId); 73 | event(new ModerationAnalysisCompleted($message, $mediaId)); 74 | break; 75 | case 'ocr': 76 | Recognize::getTextDetectionByJobId($message['JobId'], $mediaId); 77 | event(new TextAnalysisCompleted($message, $mediaId)); 78 | break; 79 | default: 80 | throw new \Exception(); 81 | } 82 | } 83 | 84 | /** 85 | * @return array 86 | */ 87 | public function ensureSubscriptionIsConfirmed(): array 88 | { 89 | $message = Message::fromRawPostData()->toArray(); 90 | 91 | if (array_key_exists('SubscribeURL', $message)) { 92 | Http::get($message['SubscribeURL']); 93 | } 94 | 95 | return json_decode($message['Message'], true); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Http/Middleware/VerifySignature.php: -------------------------------------------------------------------------------- 1 | isValid($message)) { 28 | return $next($request); 29 | } 30 | } catch (\Exception $e) { 31 | } 32 | 33 | // If you get this far it means the request is not found 34 | throw new NotFoundHttpException(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Jobs/StartFaceDetection.php: -------------------------------------------------------------------------------- 1 | path = $path; 45 | $this->mimeType = $mimeType; 46 | $this->mediaId = $mediaId; 47 | $this->faceAttribute = [$faceAttribute]; 48 | } 49 | 50 | /** 51 | * Execute the job. 52 | * 53 | * @return void 54 | * @throws \Exception 55 | */ 56 | public function handle() 57 | { 58 | $this->ensureMimeTypeIsSet(); 59 | 60 | if (Str::contains($this->mimeType, 'image')) { 61 | $result = Recognize::source($this->path, $this->mimeType, $this->mediaId)->detectImageFaces($this->faceAttribute); 62 | 63 | // we need to manually fire the event for image analyses because unlike the video analysis, 64 | // AWS is not sending a webhook upon completion of the image analysis 65 | event(new FacialAnalysisCompleted($result, $this->mediaId)); 66 | 67 | return; 68 | } 69 | 70 | if (Str::contains($this->mimeType, 'video')) { 71 | Recognize::source($this->path, $this->mimeType, $this->mediaId)->detectVideoFaces($this->faceAttribute); 72 | 73 | return; 74 | } 75 | 76 | throw new \Exception('$mimeType does neither indicate being a video nor an image'); 77 | } 78 | 79 | protected function ensureMimeTypeIsSet() 80 | { 81 | if (is_null($this->mimeType)) { 82 | $this->mimeType = Storage::disk(config('media-recognition.disk'))->mimeType($this->path); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Jobs/StartLabelDetection.php: -------------------------------------------------------------------------------- 1 | path = $path; 48 | $this->mimeType = $mimeType; 49 | $this->mediaId = $mediaId; 50 | $this->minConfidence = $minConfidence; 51 | $this->maxResults = $maxResults; 52 | } 53 | 54 | /** 55 | * Execute the job. 56 | * 57 | * @return void 58 | * @throws \Exception 59 | */ 60 | public function handle() 61 | { 62 | $this->ensureMimeTypeIsSet(); 63 | 64 | if (Str::contains($this->mimeType, 'image')) { 65 | $result = Recognize::source($this->path, $this->mimeType, $this->mediaId)->detectImageLabels($this->minConfidence, $this->maxResults); 66 | 67 | // we need to manually fire the event for image analyses because unlike the video analysis, 68 | // AWS is not sending a webhook upon completion of the image analysis 69 | event(new LabelAnalysisCompleted($result, $this->mediaId)); 70 | 71 | return; 72 | } 73 | 74 | if (Str::contains($this->mimeType, 'video')) { 75 | Recognize::source($this->path, $this->mimeType, $this->mediaId)->detectVideoLabels($this->minConfidence, $this->maxResults); 76 | 77 | return; 78 | } 79 | 80 | throw new \Exception('$mimeType does neither indicate being a video nor an image'); 81 | } 82 | 83 | protected function ensureMimeTypeIsSet() 84 | { 85 | if (is_null($this->mimeType)) { 86 | $this->mimeType = Storage::disk(config('media-recognition.disk'))->mimeType($this->path); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Jobs/StartModerationDetection.php: -------------------------------------------------------------------------------- 1 | path = $path; 45 | $this->mimeType = $mimeType; 46 | $this->mediaId = $mediaId; 47 | $this->minConfidence = $minConfidence; 48 | } 49 | 50 | /** 51 | * Execute the job. 52 | * 53 | * @return void 54 | * @throws \Exception 55 | */ 56 | public function handle() 57 | { 58 | $this->ensureMimeTypeIsSet(); 59 | 60 | if (Str::contains($this->mimeType, 'image')) { 61 | $result = Recognize::source($this->path, $this->mimeType, $this->mediaId)->detectModeration($this->minConfidence); 62 | 63 | // we need to manually fire the event for image analyses because unlike the video analysis, 64 | // AWS is not sending a webhook upon completion of the image analysis 65 | event(new ModerationAnalysisCompleted($result, $this->mediaId)); 66 | 67 | return; 68 | } 69 | 70 | if (Str::contains($this->mimeType, 'video')) { 71 | Recognize::source($this->path, $this->mimeType, $this->mediaId)->detectModeration($this->minConfidence); 72 | 73 | return; 74 | } 75 | 76 | throw new \Exception('$mimeType does neither indicate being a video nor an image'); 77 | } 78 | 79 | protected function ensureMimeTypeIsSet() 80 | { 81 | if (is_null($this->mimeType)) { 82 | $this->mimeType = Storage::disk(config('media-recognition.disk'))->mimeType($this->path); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Jobs/StartTextDetection.php: -------------------------------------------------------------------------------- 1 | path = $path; 45 | $this->mimeType = $mimeType; 46 | $this->mediaId = $mediaId; 47 | $this->filters = $filters; 48 | } 49 | 50 | /** 51 | * Execute the job. 52 | * 53 | * @return void 54 | * @throws \Exception 55 | */ 56 | public function handle() 57 | { 58 | $this->ensureMimeTypeIsSet(); 59 | 60 | if (Str::contains($this->mimeType, 'image')) { 61 | $result = Recognize::source($this->path, $this->mimeType, $this->mediaId)->detectText($this->filters); 62 | 63 | // we need to manually fire the event for image analyses because unlike the video analysis, 64 | // AWS is not sending a webhook upon completion of the image analysis 65 | event(new TextAnalysisCompleted($result, $this->mediaId)); 66 | 67 | return; 68 | } 69 | 70 | if (Str::contains($this->mimeType, 'video')) { 71 | Recognize::source($this->path, $this->mimeType, $this->mediaId)->detectText($this->filters); 72 | 73 | return; 74 | } 75 | } 76 | 77 | protected function ensureMimeTypeIsSet() 78 | { 79 | if (is_null($this->mimeType)) { 80 | $this->mimeType = Storage::disk(config('media-recognition.disk'))->mimeType($this->path); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/MediaRecognitionManager.php: -------------------------------------------------------------------------------- 1 | driver($name); 22 | } 23 | 24 | /** 25 | * Create an Amazon MediaRecognition Converter instance. 26 | * 27 | * @return \Meema\MediaRecognition\Recognizers\Rekognition 28 | * @throws \Exception 29 | */ 30 | public function createMediaRecognitionDriver(): Rekognition 31 | { 32 | $this->ensureAwsSdkIsInstalled(); 33 | 34 | $config = $this->config['media-recognition']; 35 | 36 | $credentials = $this->getCredentials($config['credentials']); 37 | 38 | $client = $this->setMediaRecognitionClient($config, $credentials); 39 | 40 | return new Rekognition($client); 41 | } 42 | 43 | /** 44 | * Sets the Recognition client. 45 | * 46 | * @param array $config 47 | * @param \Aws\Credentials\Credentials $credentials 48 | * @return \Aws\Rekognition\RekognitionClient 49 | */ 50 | protected function setMediaRecognitionClient(array $config, Credentials $credentials): RekognitionClient 51 | { 52 | return new RekognitionClient([ 53 | 'version' => $config['version'], 54 | 'region' => $config['region'], 55 | 'credentials' => $credentials, 56 | ]); 57 | } 58 | 59 | /** 60 | * Get credentials of AWS. 61 | * 62 | * @param array $credentials 63 | * @return \Aws\Credentials\Credentials 64 | */ 65 | protected function getCredentials(array $credentials): Credentials 66 | { 67 | return new Credentials($credentials['key'], $credentials['secret']); 68 | } 69 | 70 | /** 71 | * Ensure the AWS SDK is installed. 72 | * 73 | * @return void 74 | * 75 | * @throws \Exception 76 | */ 77 | protected function ensureAwsSdkIsInstalled() 78 | { 79 | if (! class_exists(RekognitionClient::class)) { 80 | throw new Exception('Please install the AWS SDK PHP using `composer require aws/aws-sdk-php`.'); 81 | } 82 | } 83 | 84 | /** 85 | * Get the default media recognition driver name. 86 | * 87 | * @return string 88 | */ 89 | public function getDefaultDriver(): string 90 | { 91 | return 'media-recognition'; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Models/MediaRecognition.php: -------------------------------------------------------------------------------- 1 | 'array', 42 | 'faces' => 'array', 43 | 'moderation' => 'array', 44 | 'ocr' => 'array', 45 | ]; 46 | 47 | public function model(): MorphTo 48 | { 49 | return $this->morphTo(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Providers/MediaRecognitionServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 21 | $this->publishes([ 22 | __DIR__.'/../../config/config.php' => config_path('media-recognition.php'), 23 | ], 'config'); 24 | } 25 | 26 | if (! class_exists('CreateMediaRecognitionsTable')) { 27 | $this->publishes([ 28 | __DIR__.'/../../database/migrations/create_media_recognitions_table.php.stub' => database_path('migrations/'.date('Y_m_d_His', time()).'_create_media_recognitions_table.php'), 29 | ], 'migrations'); 30 | } 31 | 32 | $this->loadRoutesFrom(__DIR__.'/../routes.php'); 33 | 34 | $router = $this->app->make(Router::class); 35 | 36 | if (! in_array('verify-signature', $router->getMiddleware())) { 37 | $router->aliasMiddleware('verify-signature', VerifySignature::class); 38 | } 39 | } 40 | 41 | /** 42 | * Register the application services. 43 | */ 44 | public function register() 45 | { 46 | $this->mergeConfigFrom(__DIR__.'/../../config/config.php', 'media-recognition'); 47 | 48 | $this->registerMediaRecognitionManager(); 49 | 50 | $this->registerAliases(); 51 | } 52 | 53 | /** 54 | * Registers the Text to speech manager. 55 | * 56 | * @return void 57 | */ 58 | protected function registerMediaRecognitionManager() 59 | { 60 | $this->app->singleton('recognize', function ($app) { 61 | return new MediaRecognitionManager($app); 62 | }); 63 | } 64 | 65 | /** 66 | * Register aliases. 67 | * 68 | * @return void 69 | */ 70 | protected function registerAliases() 71 | { 72 | $this->app->alias(Recognize::class, 'Recognize'); 73 | } 74 | 75 | /** 76 | * Get the services provided by the provider. 77 | * 78 | * @return array 79 | */ 80 | public function provides(): array 81 | { 82 | return [ 83 | 'recognize', 84 | ]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Recognizers/Rekognition.php: -------------------------------------------------------------------------------- 1 | client = $client; 45 | } 46 | 47 | /** 48 | * Get the MediaRecognition Client. 49 | * 50 | * @return \Aws\Rekognition\RekognitionClient 51 | */ 52 | public function getClient(): RekognitionClient 53 | { 54 | return $this->client; 55 | } 56 | 57 | /** 58 | * Detects labels/objects in an image. 59 | * 60 | * @param int|null $minConfidence 61 | * @param int|null $maxLabels 62 | * @return \Aws\Result 63 | * @throws \Exception 64 | */ 65 | public function detectLabels($minConfidence = null, $maxLabels = null) 66 | { 67 | $this->ensureMimeTypeIsSet(); 68 | 69 | if (Str::contains($this->mimeType, 'image')) { 70 | $result = $this->detectImageLabels($minConfidence, $maxLabels); 71 | 72 | // we need to manually fire the event for image analyses because unlike the video analysis, 73 | // AWS is not sending a webhook upon completion of the image analysis 74 | event(new LabelAnalysisCompleted($result->toArray(), $this->mediaId)); 75 | 76 | return $result; 77 | } 78 | 79 | if (Str::contains($this->mimeType, 'video')) { 80 | return $this->detectVideoLabels($minConfidence, $maxLabels); 81 | } 82 | 83 | throw new \Exception('$mimeType does neither indicate being a video nor an image'); 84 | } 85 | 86 | /** 87 | * Detects faces in an image & analyzes them. 88 | * 89 | * @param array $attributes 90 | * @return \Aws\Result 91 | * @throws \Exception 92 | */ 93 | public function detectFaces($attributes = ['DEFAULT']) 94 | { 95 | $this->ensureMimeTypeIsSet(); 96 | 97 | if (Str::contains($this->mimeType, 'image')) { 98 | $result = $this->detectImageFaces($attributes); 99 | 100 | // we need to manually fire the event for image analyses because unlike the video analysis, 101 | // AWS is not sending a webhook upon completion of the image analysis 102 | event(new FacialAnalysisCompleted($result->toArray(), $this->mediaId)); 103 | 104 | return $result; 105 | } 106 | 107 | if (Str::contains($this->mimeType, 'video')) { 108 | $attribute = is_array($attributes) ? $attributes[0] : $attributes; 109 | 110 | return $this->detectVideoFaces($attribute); 111 | } 112 | 113 | throw new \Exception('$mimeType does neither indicate being a video nor an image'); 114 | } 115 | 116 | /** 117 | * Detects moderation labels in an image. 118 | * This can be useful for children-friendly images or NSFW images. 119 | * 120 | * @param int|null $minConfidence 121 | * @return \Aws\Result 122 | * @throws \Exception 123 | */ 124 | public function detectModeration($minConfidence = null) 125 | { 126 | $this->ensureMimeTypeIsSet(); 127 | 128 | if (Str::contains($this->mimeType, 'image')) { 129 | $result = $this->detectImageModeration($minConfidence); 130 | 131 | // we need to manually fire the event for image analyses because unlike the video analysis, 132 | // AWS is not sending a webhook upon completion of the image analysis 133 | event(new ModerationAnalysisCompleted($result->toArray(), $this->mediaId)); 134 | 135 | return $result; 136 | } 137 | 138 | if (Str::contains($this->mimeType, 'video')) { 139 | return $this->detectVideoModeration($minConfidence); 140 | } 141 | 142 | throw new \Exception('$mimeType does neither indicate being a video nor an image'); 143 | } 144 | 145 | /** 146 | * Detects text in an image (OCR). 147 | * 148 | * @param array|null $filters 149 | * @return \Aws\Result 150 | * @throws \Exception 151 | */ 152 | public function detectText(array $filters = null) 153 | { 154 | $this->ensureMimeTypeIsSet(); 155 | 156 | if (Str::contains($this->mimeType, 'image')) { 157 | $result = $this->detectImageText($filters); 158 | 159 | // we need to manually fire the event for image analyses because unlike the video analysis, 160 | // AWS is not sending a webhook upon completion of the image analysis 161 | event(new TextAnalysisCompleted($result->toArray(), $this->mediaId)); 162 | 163 | return $result; 164 | } 165 | 166 | if (Str::contains($this->mimeType, 'video')) { 167 | return $this->detectVideoText($filters); 168 | } 169 | 170 | throw new \Exception('$mimeType does neither indicate being a video nor an image'); 171 | } 172 | 173 | /** 174 | * @param $type 175 | * @param $results 176 | * @return mixed 177 | * @throws \Exception 178 | */ 179 | protected function updateOrCreate($type, $results) 180 | { 181 | MediaRecognition::updateOrCreate([ 182 | 'model_id' => $this->mediaId, 183 | 'model_type' => config('media-converter.media_model'), 184 | ], [$type => $results->toArray()]); 185 | 186 | return $results; 187 | } 188 | 189 | /** 190 | * @param string $jobId 191 | * @param string $type 192 | * @return void 193 | * @throws \Exception 194 | */ 195 | protected function updateJobId(string $jobId, string $type) 196 | { 197 | if (is_null($this->mediaId)) { 198 | return; 199 | } 200 | 201 | MediaRecognition::updateOrCreate([ 202 | 'model_id' => $this->mediaId, 203 | 'model_type' => config('media-converter.media_model'), 204 | ], [$type.'_job_id' => $jobId]); 205 | } 206 | 207 | /** 208 | * @param array $results 209 | * @param string $type 210 | * @param int $mediaId 211 | * @return void 212 | */ 213 | protected function updateVideoResults(array $results, string $type, int $mediaId) 214 | { 215 | $mediaRecognition = MediaRecognition::where('model_id', $mediaId)->firstOrFail(); 216 | $mediaRecognition->$type = $results; 217 | $mediaRecognition->save(); 218 | } 219 | 220 | protected function ensureMimeTypeIsSet() 221 | { 222 | if (is_null($this->mimeType)) { 223 | $this->mimeType = Storage::disk(config('media-recognition.disk'))->mimeType($this->source); 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Traits/CanRecognizeImages.php: -------------------------------------------------------------------------------- 1 | blob = $blob; 26 | 27 | return $this; 28 | } 29 | 30 | /** 31 | * Sets the image to be analyzed. 32 | * 33 | * @return void 34 | * @throws \Exception 35 | */ 36 | protected function setImageSettings(): void 37 | { 38 | $this->ensureSourceIsNotNull(); 39 | 40 | if (is_string($this->blob)) { 41 | $this->settings['Image'] = [ 42 | 'Bytes' => $this->blob, 43 | ]; 44 | 45 | return; 46 | } 47 | 48 | $disk = $this->disk ?? config('media-recognition.disk'); 49 | $bucketName = config("filesystems.disks.$disk.bucket"); 50 | 51 | if (! $bucketName) { 52 | throw new Exception('Please make sure to set a S3 bucket name.'); 53 | } 54 | 55 | $this->settings['Image'] = [ 56 | 'S3Object' => [ 57 | 'Bucket' => $bucketName, 58 | 'Name' => $this->source, 59 | ], 60 | ]; 61 | } 62 | 63 | /** 64 | * @param int|null $minConfidence 65 | * @param null $maxLabels 66 | * @return mixed 67 | * @throws \Exception 68 | */ 69 | public function detectImageLabels($minConfidence = null, $maxLabels = null) 70 | { 71 | $this->setImageSettings(); 72 | 73 | $this->settings['MinConfidence'] = $minConfidence ?? config('media-recognition.min_confidence'); 74 | 75 | if (is_int($maxLabels)) { 76 | $this->settings['MaxLabels'] = $maxLabels; 77 | } 78 | 79 | $results = $this->client->detectLabels($this->settings); 80 | 81 | if (is_null($this->mediaId)) { 82 | return $results; 83 | } 84 | 85 | $this->updateOrCreate('labels', $results); 86 | 87 | return $results; 88 | } 89 | 90 | /** 91 | * @param array $attributes 92 | * @return mixed 93 | * @throws \Exception 94 | */ 95 | public function detectImageFaces($attributes = ['DEFAULT']) 96 | { 97 | $this->setImageSettings(); 98 | 99 | $this->settings['Attributes'] = $attributes; 100 | 101 | $results = $this->client->detectFaces($this->settings); 102 | 103 | if (is_null($this->mediaId)) { 104 | return $results; 105 | } 106 | 107 | $this->updateOrCreate('faces', $results); 108 | 109 | return $results; 110 | } 111 | 112 | /** 113 | * @param int|null $minConfidence 114 | * @return mixed 115 | * @throws \Exception 116 | */ 117 | public function detectImageModeration($minConfidence = null) 118 | { 119 | $this->setImageSettings(); 120 | 121 | $this->settings['MinConfidence'] = $minConfidence ?? config('media-recognition.min_confidence'); 122 | 123 | $results = $this->client->detectModerationLabels($this->settings); 124 | 125 | if (is_null($this->mediaId)) { 126 | return $results; 127 | } 128 | 129 | $this->updateOrCreate('moderation', $results); 130 | 131 | return $results; 132 | } 133 | 134 | /** 135 | * @param array|null $filters 136 | * @return mixed 137 | * @throws \Exception 138 | */ 139 | public function detectImageText(array $filters = null) 140 | { 141 | $this->setImageSettings(); 142 | 143 | if (is_array($filters)) { 144 | $this->settings['Filters'] = $filters; 145 | } 146 | 147 | $results = $this->client->detectText($this->settings); 148 | 149 | if (is_null($this->mediaId)) { 150 | return $results; 151 | } 152 | 153 | $this->updateOrCreate('ocr', $results); 154 | 155 | return $results; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Traits/CanRecognizeVideos.php: -------------------------------------------------------------------------------- 1 | disk ?? config('media-recognition.disk'); 20 | $bucketName = config("filesystems.disks.$disk.bucket"); 21 | 22 | if (! $bucketName) { 23 | throw new Exception('Please make sure to set a S3 bucket name.'); 24 | } 25 | 26 | $this->settings['Video'] = [ 27 | 'S3Object' => [ 28 | 'Bucket' => $bucketName, 29 | 'Name' => $this->source, 30 | ], 31 | ]; 32 | 33 | $this->settings['NotificationChannel'] = [ 34 | 'RoleArn' => config('media-recognition.iam_arn'), 35 | 'SNSTopicArn' => config('media-recognition.sns_topic_arn'), 36 | ]; 37 | 38 | $uniqueId = $type.'_'.$this->mediaId.'_'.Str::random(6); 39 | 40 | // Idempotent token used to identify the start request. 41 | // If you use the same token with multiple StartCelebrityRecognition requests, the same JobId is returned. 42 | // Use ClientRequestToken to prevent the same job from being accidentally started more than once. 43 | $this->settings['ClientRequestToken'] = $uniqueId; 44 | 45 | // the JobTag is set to be the media id, so we can adjust the media record with the results once the webhook comes in 46 | $this->settings['JobTag'] = $uniqueId; 47 | } 48 | 49 | /** 50 | * Starts asynchronous detection of labels/objects in a stored video. 51 | * 52 | * @param int|null $minConfidence 53 | * @param int $maxResults 54 | * @return \Aws\Result 55 | * @throws \Exception 56 | */ 57 | public function detectVideoLabels($minConfidence = null, $maxResults = 1000) 58 | { 59 | $this->setVideoSettings('labels'); 60 | $this->settings['MinConfidence'] = $minConfidence ?? config('media-recognition.min_confidence'); 61 | $this->settings['MaxResults'] = $maxResults; 62 | 63 | $results = $this->client->startLabelDetection($this->settings); 64 | 65 | if ($results['JobId']) { 66 | $this->updateJobId($results['JobId'], 'labels'); 67 | } 68 | 69 | return $results; 70 | } 71 | 72 | /** 73 | * @param array $attributes 74 | * @return mixed 75 | * @throws \Exception 76 | */ 77 | public function detectVideoFaces($attributes = 'DEFAULT') 78 | { 79 | $this->setVideoSettings('faces'); 80 | 81 | $this->settings['FaceAttributes'] = $attributes; 82 | 83 | $results = $this->client->startFaceDetection($this->settings); 84 | 85 | if ($results['JobId']) { 86 | $this->updateJobId($results['JobId'], 'faces'); 87 | } 88 | 89 | return $results; 90 | } 91 | 92 | /** 93 | * Starts asynchronous detection of unsafe content in a stored video. 94 | * 95 | * @param int|null $minConfidence 96 | * @return mixed 97 | * @throws \Exception 98 | */ 99 | public function detectVideoModeration($minConfidence = null) 100 | { 101 | $this->setVideoSettings('moderation'); 102 | 103 | $this->settings['MinConfidence'] = $minConfidence ?? config('media-recognition.min_confidence'); 104 | 105 | $results = $this->client->startContentModeration($this->settings); 106 | 107 | if ($results['JobId']) { 108 | $this->updateJobId($results['JobId'], 'moderation'); 109 | } 110 | 111 | return $results; 112 | } 113 | 114 | /** 115 | * Starts asynchronous detection of text in a stored video. 116 | * 117 | * @param array|null $filters 118 | * @return mixed 119 | * @throws \Exception 120 | */ 121 | public function detectVideoText(array $filters = null) 122 | { 123 | $this->setVideoSettings('ocr'); 124 | 125 | if (is_array($filters)) { 126 | $this->settings['Filters'] = $filters; 127 | } 128 | 129 | $results = $this->client->startTextDetection($this->settings); 130 | 131 | if ($results['JobId']) { 132 | $this->updateJobId($results['JobId'], 'ocr'); 133 | } 134 | 135 | return $results; 136 | } 137 | 138 | /** 139 | * Get the labels from the video analysis. 140 | * 141 | * @param string $jobId 142 | * @param int|null $mediaId 143 | * @return \Aws\Result 144 | * @throws \Exception 145 | */ 146 | public function getLabelsByJobId(string $jobId, int $mediaId = null) 147 | { 148 | $results = $this->client->getLabelDetection([ 149 | 'JobId' => $jobId, 150 | ]); 151 | 152 | if (is_null($mediaId)) { 153 | return $results; 154 | } 155 | 156 | $this->updateVideoResults($results->toArray(), 'labels', $mediaId); 157 | 158 | return $results; 159 | } 160 | 161 | /** 162 | * Get the faces from the video analysis. 163 | * 164 | * @param string $jobId 165 | * @param int|null $mediaId 166 | * @return \Aws\Result 167 | * @throws \Exception 168 | */ 169 | public function getFacesByJobId(string $jobId, int $mediaId = null) 170 | { 171 | $results = $this->client->getFaceDetection([ 172 | 'JobId' => $jobId, 173 | ]); 174 | 175 | if (is_null($mediaId)) { 176 | return $results; 177 | } 178 | 179 | $this->updateVideoResults($results->toArray(), 'faces', $mediaId); 180 | 181 | return $results; 182 | } 183 | 184 | /** 185 | * Get the "content moderation" from the video analysis. 186 | * 187 | * @param string $jobId 188 | * @param int|null $mediaId 189 | * @return \Aws\Result 190 | * @throws \Exception 191 | */ 192 | public function getContentModerationByJobId(string $jobId, int $mediaId = null) 193 | { 194 | $results = $this->client->getContentModeration([ 195 | 'JobId' => $jobId, 196 | ]); 197 | 198 | if (is_null($mediaId)) { 199 | return $results; 200 | } 201 | 202 | $this->updateVideoResults($results->toArray(), 'moderation', $mediaId); 203 | 204 | return $results; 205 | } 206 | 207 | /** 208 | * Get the faces from a video analysis. 209 | * 210 | * @param string $jobId 211 | * @param int|null $mediaId 212 | * @return \Aws\Result 213 | * @throws \Exception 214 | */ 215 | public function getTextDetectionByJobId(string $jobId, int $mediaId = null) 216 | { 217 | $results = $this->client->getTextDetection([ 218 | 'JobId' => $jobId, 219 | ]); 220 | 221 | if (is_null($mediaId)) { 222 | return $results; 223 | } 224 | 225 | $this->updateVideoResults($results->toArray(), 'ocr', $mediaId); 226 | 227 | return $results; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/Traits/InteractsWithStorage.php: -------------------------------------------------------------------------------- 1 | disk = $disk; 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * The equivalent of the S3 Key / the path of the file inside the bucket. 50 | * 51 | * @param string $source 52 | * @param string|null $mimeType 53 | * @param int|null $mediaId 54 | * @return $this 55 | */ 56 | public function source(string $source, string $mimeType = null, int $mediaId = null) 57 | { 58 | $this->source = $source; 59 | $this->mimeType = $mimeType; 60 | $this->mediaId = $mediaId; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Alias of source(). 67 | * 68 | * @param string $source 69 | * @param string|null $mimeType 70 | * @param int|null $mediaId 71 | * @return $this 72 | */ 73 | public function path(string $source, string $mimeType = null, int $mediaId = null) 74 | { 75 | return $this->source($source, $mimeType, $mediaId); 76 | } 77 | 78 | /** 79 | * Ensures the source/path not to be null if it is null it will thrown an exception. 80 | * 81 | * @return void 82 | * @throws \Exception 83 | */ 84 | public function ensureSourceIsNotNull() 85 | { 86 | if (is_null($this->source)) { 87 | throw new \Exception('Please set a $source to run the analysis on'); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Traits/Recognizable.php: -------------------------------------------------------------------------------- 1 | morphOne(MediaRecognition::class, 'model'); 18 | } 19 | 20 | /** 21 | * Start a media "recognition". 22 | * 23 | * @param string $path 24 | * @param string|null $mimeType 25 | * @return \Meema\MediaRecognition\Contracts\MediaRecognition 26 | */ 27 | public function recognize(string $path, string $mimeType = null) 28 | { 29 | return Recognize::source($path, $mimeType, $this->id); 30 | } 31 | 32 | /** 33 | * Return all recognition data. 34 | * The return value "null" indicates that a recognition has been ran, but it just has no results. 35 | * Please note, the "Facial Rekognition" response is different from a "video" to an "image". 36 | * 37 | * @return array 38 | */ 39 | public function recognitionData(): array 40 | { 41 | $recognition = $this->recognition()->first(); 42 | 43 | if (! $recognition) { 44 | return [ 45 | 'labels' => null, 46 | 'faces' => null, 47 | 'moderation' => null, 48 | 'texts' => null, 49 | ]; 50 | } 51 | 52 | // null indicates that the "recognition" has not been ran for the category 53 | $labels = $faces = $moderation = $texts = null; 54 | 55 | if ($recognition->labels && is_array($recognition->labels['Labels'])) { 56 | $labels = $recognition->labels['Labels']; 57 | } 58 | 59 | if ($recognition->faces && is_array($recognition->faces['FaceDetails'])) { 60 | $faces = $recognition->faces['FaceDetails']; 61 | } elseif ($recognition->faces && is_array($recognition->faces['Faces'])) { 62 | $faces = $recognition->faces['Faces']; 63 | } 64 | 65 | if ($recognition->moderation && is_array($recognition->moderation['ModerationLabels'])) { 66 | $moderation = $recognition->moderation['ModerationLabels']; 67 | } 68 | 69 | if ($recognition->ocr && is_array($recognition->ocr['TextDetections'])) { 70 | $texts = $recognition->ocr['TextDetections']; 71 | } 72 | 73 | return [ 74 | 'labels' => $labels, 75 | 'faces' => $faces, 76 | 'moderation' => $moderation, 77 | 'texts' => $texts, 78 | ]; 79 | } 80 | 81 | public function minimalRecognitionData( 82 | bool $includeLabels = true, 83 | bool $includeFaces = true, 84 | bool $includeModeration = true, 85 | bool $includeTexts = true, 86 | int $limit = 50 87 | ): array { 88 | $data = $this->recognitionData(); 89 | 90 | if (! $data) { 91 | return [ 92 | 'labels' => null, 93 | 'faces' => null, 94 | 'moderation' => null, 95 | 'texts' => null, 96 | ]; 97 | } 98 | 99 | $array = []; 100 | 101 | if ($includeLabels) { 102 | $array['labels'] = collect($data['labels'])->map(function ($label) { 103 | return $this->generateLabelElement($label); 104 | })->unique('name')->sortByDesc('confidence')->take($limit)->values()->toArray(); 105 | } 106 | 107 | if ($includeFaces) { 108 | $array['faces'] = collect($data['faces'])->map(function ($face) { 109 | return $this->generateFaceElement($face); 110 | })->sortByDesc('confidence')->take($limit)->values()->toArray(); 111 | } 112 | 113 | if ($includeModeration) { 114 | $array['moderation'] = collect($data['moderation'])->map(function ($moderation) { 115 | return $this->generateModerationElement($moderation); 116 | })->unique('name')->sortByDesc('confidence')->take($limit)->values()->toArray(); 117 | } 118 | 119 | if ($includeTexts) { 120 | $array['texts'] = collect($data['texts'])->map(function ($text) { 121 | return $this->generateTextElement($text); 122 | })->unique('text')->sortByDesc('confidence')->take($limit)->values()->toArray(); 123 | } 124 | 125 | return $array; 126 | } 127 | 128 | public function generateLabelElement($label): array 129 | { 130 | // image element 131 | if ($label['Name']) { 132 | return [ 133 | 'name' => $label['Name'], 134 | 'confidence' => $label['Confidence'], 135 | 'timestamp' => null, // timestamps are only available in videos 136 | ]; 137 | } 138 | 139 | // video element 140 | return [ 141 | 'name' => $label['Label']['Name'], 142 | 'confidence' => $label['Label']['Confidence'], 143 | 'timestamp' => $label['Timestamp'], 144 | ]; 145 | } 146 | 147 | public function generateFaceElement($face): array 148 | { 149 | // image element 150 | if ($face['BoundingBox']) { 151 | return [ 152 | 'bounding_box' => $face['BoundingBox'], 153 | 'confidence' => $face['Confidence'], 154 | 'timestamp' => null, // timestamps are only available in videos 155 | ]; 156 | } 157 | 158 | // video element 159 | return [ 160 | 'bounding_box' => $face['Face']['BoundingBox'], 161 | 'confidence' => $face['Face']['Confidence'], 162 | 'timestamp' => $face['Timestamp'], 163 | ]; 164 | } 165 | 166 | public function generateModerationElement($moderation): array 167 | { 168 | // image element 169 | if ($moderation['Name']) { 170 | return [ 171 | 'name' => $moderation['Name'], 172 | 'confidence' => $moderation['Confidence'], 173 | 'timestamp' => null, // timestamps are only available in videos 174 | ]; 175 | } 176 | 177 | // video element 178 | return [ 179 | 'name' => $moderation['ModerationLabel']['Name'], 180 | 'confidence' => $moderation['ModerationLabel']['Confidence'], 181 | 'timestamp' => $moderation['Timestamp'], 182 | ]; 183 | } 184 | 185 | public function generateTextElement($text): array 186 | { 187 | // image element 188 | if ($text['DetectedText']) { 189 | return [ 190 | 'text' => $text['DetectedText'], 191 | 'confidence' => $text['Confidence'], 192 | 'timestamp' => null, // timestamps are only available in videos 193 | ]; 194 | } 195 | 196 | // video element 197 | return [ 198 | 'text' => $text['TextDetection']['DetectedText'], 199 | 'confidence' => $text['TextDetection']['Confidence'], 200 | 'timestamp' => $text['Timestamp'], 201 | ]; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/routes.php: -------------------------------------------------------------------------------- 1 | name('webhooks.media-recognition'); 7 | --------------------------------------------------------------------------------