We're happy to share that we are launching a new line of AI bots in Tech Preview. We've had one AI Robot in production for some time: /image/facedetect. It's powered by internal software and we've been pleased with its performance. Today's announced bots, however, are powered by external cloud services.

It's hard to miss the AI advancements that the The Big Five are making. With access to virtually unlimited data, models can be trained to achieve unparalleled accuracy. We felt that offering these AI capabilites right inside our encoding pipelines could add tremendous value to customers seeking to further automate their media processing.

We've tested and mapped out AI offerings by the Google Cloud Platform (GCP) and Amazon Web Services (AWS), and started drawing Venn diagrams to pinpoint overlapping functionality.

Our idea was to offer an abstraction over the lowest common denominator. In other words: with a single API, our customers can plug in, say, image recognition of either provider, and get back uniform responses. It would be just a matter of specifying either provider: 'aws' or provider: 'gcp' to switch from one provider to the next.

Why let Transloadit wrap this?

It goes without saying that Transloadit cannot beat or even meet the pricing of the AI providers themselves, so if you need to process massive amounts of data, consider integrating with GCP/AWS directly.

However, if your use case doesn't revolve around squeezing every last penny out of every last byte, there are four reasons why our customers may want to use these AI services in conjunction with Transloadit:

1. LEGO-like composability

You can drop AI in existing encoding pipelines, mixing and matching with 53 features (Robots) to create workflows unique to your business. All of this without writing imperative boilerplate code to string it all together, which would result in more moving parts and points of failure.

Transloadit offers an integral solution that can be wielded with a single deterministic JSON recipe. With twenty lines of declarative instructions, you could order Transloadit to pass a video through these Robots:

  1. /speech/transcribe: turn the video into human-readable text
  2. /text/translate: translate the text into Japanese
  3. /text/speak: synthesize the Japanese into spoken language
  4. /video/merge: merge the new spoken Japanese as an audio track over the original video

Essentially, you have now made Transloadit translate a video automatically :smile: It's probably not ready for prime time, but this does illustrate how powerful our Assembly line can be. For code samples around our declarative composability, check further down.

2. Easily compare and switch between AI providers

The vendors use different notations for languages (when translating), they structure their responses differently and they have different docs, SDKs, formats, settings, etc.

Transloadit abstracts all of this and accepts uniform input, delivering uniform output — no matter the provider.

After having used Amazon, you can see how Google describes the same image without changing anything but the provider parameter. This way, you can easily compare results and latencies in your app, to see what bests suits your use case. And that could change of course. These AIs are constantly learning and improving for the majority of cases, but if one of your own customers has an unlucky minority case, you could offer to switch in a heartbeat.

3. Possibly cut down on vendors

If you are either:

  • already using Transloadit for your media processing
  • in the market for an AI feature but would also like to augment that with automated image optimization, encoding, or leverage any of our other 53 features

.. this saves you the hassle of integrating with yet another provider. We already indicated the engineering costs associated with many moving parts, but there is also different billing to consider, SLA agreements to monitor, and support desks to deal with.

4. Hassle-free :massage:

We automatically sanitize and cleanup inputs. For instance, while AWS will accept any audio file to transcribe, depending on settings, Google will want it in the PCM format with signed 16-bit, 1-channel, little-endian encoding. With Transloadit, you just throw any audio (or video!) at us, and we'll make sure it gets converted to whatever way the AI provider you picked, likes it.

Features we are launching today

Today, we are launching two Robots in Tech Preview:

Our /image/ describe Robot

Our /image/describe Robot. Input an image and get back a list of objects that were detected: Tree, Car, House, etc. We can return it as a text file, JSON file, or pass it to another Robot for processing. Common use cases include automatically flagging (in)appropriate content, providing alt captions for images, and/or making images searchable.

Our /speech/transcribe Robot

Our /speech/transcribe Robot. Input an audio or video recording and get back human-readable text. We can return it as a text file, JSON file, or pass it to another Robot for processing. Common use cases include automated subtitling, or making audio/video searchable.

We're launching them in conjunction with an upgrade to:

Our /file/filter Robot

Our /file/filter Robot. Pass it a file and criteria, and this Robot acts as a gatekeeper, optionally passing files through to another Step, like exporting. We changed it so that it now also takes an includes operator.

With the newly added includes operator, you can now start automatically rejecting (or flagging) undesired content like so:

"described": {
  "use": ":original",
  "robot": "/image/describe",
  "provider": "aws",
  "format": "meta",
  "granularity": "list"
},
"filtered": {
  "use": "described",
  "robot": "/file/filter",
  "declines": [
    [ "${file.meta.descriptions}", "includes", [ "Naked", "Sex" ] ]
  ]
},
"exported": {
  "use": "filtered",
  "robot": "/s3/store",
  "credentials": "YOUR_AWS_CREDENTIALS"
}

Now, if I wanted to only allow pictures of cars for my used cars sales website, and I preferred Google's image recognition, I'd just change:

  • "declines" to "accepts"
  • [ "Naked", "Sex" ] to [ "Car", "Tires" ]
  • "provider": "aws" to "provider": "gcp"

And that's it! :sparkles:

We also have a full code sample featuring our /image/describe Robot further down, as well as links to demos for our /speech/transcribe and /image/facedetect Robots.

What AI features are planned?

Besides the two Robots launched today in Tech Preview, our Venn diagrams have showed us we should also build the following:

  • /image/ocr: input an image, get back any human-readable text that it had on it, like name/traffic signs
  • /document/ocr: input a PDF, get back any human-readable text that it had on it, so that documents can be made searchable if they aren't already
  • /text/translate: input human-readable text and get it back in a different language
  • /text/speak: input human-readable text and get back an audio file with a recording of synthesized speech

Missing something on this list? We're happy to take suggestions for more!

What about pricing?

We track input and output bytes passing through these Robots, and subtract that from your regular Transloadit plan — no need for any extra subscriptions. We do charge a minimum fee of 1MB per transaction: if you submit a 100KB image, and a 2KB text file is returned, even though that adds up to 102KB, we still subtract 1MB from your plan. On our Startup Plan (10GB for $49/mo) that would have costed $0.0049, on our Medium Business Plan $0.00166. More info on our Pricing page.

What about other providers like Microsoft Azure?

We feel there's enough value here to start offering this as Tech Preview today. Since we abstract the providers, we're not dependent on a single offering. So should GCP be shut down in 2023 (just kidding! we think!) or AWS raises prices on us, there are options. Integrations remain the same when switching providers. In fact, we are looking into adding Microsoft Azure into the mix as well.

And, just like we are powering our /image/facedetect AI Robot ourselves, when it becomes feasible in the future to run high-quality transcription AI ourselves, you may find we add a provider: 'transloadit' to the /speech/transcribe Robot, offered at a lower price.

What does "Tech Preview" mean?

It means that you can start using this tech today! We might still make changes to the API and pricing (but we do not foresee them outside of adding more features).

How do I get started?

After signing up, pick a programming language of choice, and crank out an integration! Sounds hard? Let's look at a demo.

{
  ":original": {
    "robot": "/upload/handle",
    "result": true
  },
  "described": {
    "use": ":original",
    "robot": "/image/describe",
    "provider": "aws",
    "format": "meta",
    "granularity": "list",
    "result": true
  },
  "filtered": {
    "result": true,
    "use": "described",
    "robot": "/file/filter",
    "accepts": [
      [
        "${file.meta.descriptions}",
        "includes",
        "Bridge"
      ]
    ]
  },
  "exported": {
    "use": "filtered",
    "robot": "/s3/store",
    "credentials": "YOUR_AWS_CREDENTIALS",
    "url_prefix": "https://demos.transloadit.com/"
  }
}
# Prerequisites: brew install curl jq || sudo apt install curl jq
# To avoid tampering, use Signature Authentication
echo '{
  "auth": {
    "key": "YOUR_TRANSLOADIT_KEY"
  },
  "steps": {
    ":original": {
      "robot": "/upload/handle",
      "result": true
    },
    "described": {
      "use": ":original",
      "robot": "/image/describe",
      "provider": "aws",
      "format": "meta",
      "granularity": "list",
      "result": true
    },
    "filtered": {
      "result": true,
      "use": "described",
      "robot": "/file/filter",
      "accepts": [
        [
          "${file.meta.descriptions}",
          "includes",
          "Bridge"
        ]
      ]
    },
    "exported": {
      "use": "filtered",
      "robot": "/s3/store",
      "credentials": "YOUR_AWS_CREDENTIALS",
      "url_prefix": "https://demos.transloadit.com/"
    }
  }
}' |curl \
    --request POST \
    --form params=@- \
    --form my_file1=@./prinsengracht.jpg \
    --form my_file2=@./chameleon.jpg \
  https://api2.transloadit.com/assemblies \
|jq
// Add 'Transloadit' to your Podfile, run 'pod install', add credentials to 'Info.plist'
import Arcane
import TransloaditKit

// Set Encoding Instructions
var AssemblySteps: Array = Array<Step>() // An array to hold the Steps

var Step1 = Step (key: ":original") // Create a Step object
Step1?.setValue("/upload/handle", forOption: "robot") // Add the details
Step1?.setValue(true, forOption: "result") // Add the details
AssemblySteps.append(Step1) // Add the Step to the array

var Step2 = Step (key: "described") // Create a Step object
Step2?.setValue(":original", forOption: "use") // Add the details
Step2?.setValue("/image/describe", forOption: "robot") // Add the details
Step2?.setValue(true, forOption: "result") // Add the details
Step2?.setValue("meta", forOption: "format") // Add the details
Step2?.setValue("list", forOption: "granularity") // Add the details
Step2?.setValue("aws", forOption: "provider") // Add the details
AssemblySteps.append(Step2) // Add the Step to the array

var Step3 = Step (key: "filtered") // Create a Step object
Step3?.setValue("described", forOption: "use") // Add the details
Step3?.setValue("/file/filter", forOption: "robot") // Add the details
Step3?.setValue(true, forOption: "result") // Add the details
Step3?.setValue([["${file.meta.descriptions}","includes","Bridge"]], forOption: "accepts") // Add the details
AssemblySteps.append(Step3) // Add the Step to the array

var Step4 = Step (key: "exported") // Create a Step object
Step4?.setValue("filtered", forOption: "use") // Add the details
Step4?.setValue("/s3/store", forOption: "robot") // Add the details
Step4?.setValue("YOUR_AWS_CREDENTIALS", forOption: "credentials") // Add the details
Step4?.setValue("https://demos.transloadit.com/", forOption: "url_prefix") // Add the details
AssemblySteps.append(Step4) // Add the Step to the array

// We then create an Assembly Object with the Steps and files
var MyAssembly: Assembly = Assembly(steps: AssemblySteps, andNumberOfFiles: 1)

// Add files to upload
MyAssembly.addFile("./prinsengracht.jpg")
MyAssembly.addFile("./chameleon.jpg")

// Start the Assembly
Transloadit.createAssembly(MyAssembly) 

// Fires after your Assembly has completed
transloadit.assemblyStatusBlock = {(_ completionDictionary: [AnyHashable: Any]) -> Void in
  print("\(completionDictionary.description)")
}
<body>
  <form action="/uploads" enctype="multipart/form-data" method="POST">
    <input type="file" name="my_file" multiple="multiple" />
  </form>

  <script src="//ajax.googleapis.com/ajax/libs/jquery/3.2.0/jquery.min.js"></script>
  <script src="//assets.transloadit.com/js/jquery.transloadit2-v3-latest.js"></script>
  <script type="text/javascript">
  $(function() {
    $('form').transloadit({
      wait: true,
      triggerUploadOnFileSelection: true,
      params: {
        auth: {
          // To avoid tampering use signatures:
          // https://transloadit.com/docs/api/#authentication
          key: 'YOUR_TRANSLOADIT_KEY',
        },
        // It's often better store encoding instructions in your account
        // and use a `template_id` instead of adding these steps inline
        steps: {
          ':original': {
            robot: '/upload/handle',
            result: true
          },
          described: {
            use: ':original',
            robot: '/image/describe',
            result: true,
            format: 'meta',
            granularity: 'list',
            provider: 'aws'
          },
          filtered: {
            use: 'described',
            robot: '/file/filter',
            result: true,
            accepts: [['${file.meta.descriptions}','includes','Bridge']]
          },
          exported: {
            use: 'filtered',
            robot: '/s3/store',
            credentials: 'YOUR_AWS_CREDENTIALS',
            url_prefix: 'https://demos.transloadit.com/'
          }
        }
      }
    });
  });
  </script>
</body>
<!-- This pulls Uppy from our CDN. Alternatively use `npm i @uppy/robodog --save` -->
<!-- if you want smaller self-hosted bundles and/or to use modern JavaScript -->
<link href="//transloadit.edgly.net/releases/uppy/robodog/v1.6.7/robodog.min.css" rel="stylesheet">
<script src="//transloadit.edgly.net/releases/uppy/robodog/v1.6.7/robodog.min.js"></script>
<button id="browse">Select Files</button>
<script>
  document.getElementById('browse').addEventListener('click', function () {
    var uppy = window.Robodog.pick({
      providers: [ 'instagram', 'url', 'webcam', 'dropbox', 'google-drive', 'facebook', 'onedrive' ],
      waitForEncoding: true,
      params: {
        // To avoid tampering, use Signature Authentication
        auth: { key: 'YOUR_TRANSLOADIT_KEY' },
        // To hide your `steps`, use a `template_id` instead
        steps: {
          ':original': {
            robot: '/upload/handle',
            result: true
          },
          described: {
            use: ':original',
            robot: '/image/describe',
            result: true,
            format: 'meta',
            granularity: 'list',
            provider: 'aws'
          },
          filtered: {
            use: 'described',
            robot: '/file/filter',
            result: true,
            accepts: [['${file.meta.descriptions}', 'includes', 'Bridge']]
          },
          exported: {
            use: 'filtered',
            robot: '/s3/store',
            credentials: 'YOUR_AWS_CREDENTIALS',
            url_prefix: 'https://demos.transloadit.com/'
          }
        }
      }
    }).then(function (bundle) {
      // Due to `waitForEncoding: true` this is fired after encoding is done.
      // Alternatively, set `waitForEncoding` to `false` and provide a `notify_url`
      // for Async Mode where your back-end receives the encoding results
      // so that your user can be on their way as soon as the upload completes.
      console.log(bundle.transloadit) // Array of Assembly Statuses
      console.log(bundle.results)     // Array of all encoding results
    }).catch(console.error)
  })
</script>
// yarn add transloadit || npm i transloadit --save-exact
const Transloadit = require('transloadit')

const transloadit = new Transloadit({
  authKey: 'YOUR_TRANSLOADIT_KEY',
  authSecret: 'YOUR_TRANSLOADIT_SECRET'
})

// Set Encoding Instructions
const options = {
  params: {
    steps: {
      ':original': {
        robot: '/upload/handle',
        result: true,
      },
      described: {
        use: ':original',
        robot: '/image/describe',
        result: true,
        format: 'meta',
        granularity: 'list',
        provider: 'aws',
      },
      filtered: {
        use: 'described',
        robot: '/file/filter',
        result: true,
        accepts: [['${file.meta.descriptions}','includes','Bridge']],
      },
      exported: {
        use: 'filtered',
        robot: '/s3/store',
        credentials: 'YOUR_AWS_CREDENTIALS',
        url_prefix: 'https://demos.transloadit.com/',
      },
    }
  }
}

// Add files to upload
transloadit.addFile('myfile_1', './prinsengracht.jpg')
transloadit.addFile('myfile_2', './chameleon.jpg')

// Start the Assembly
transloadit.createAssembly(options, (err, result) => {
  if (err) {
    throw err
  }

  console.log({result})
})
[sudo] npm install transloadify -g

export TRANSLOADIT_KEY="YOUR_TRANSLOADIT_KEY"
export TRANSLOADIT_SECRET="YOUR_TRANSLOADIT_SECRET"

# Save Encoding Instructions
echo '{
  ":original": {
    "robot": "/upload/handle",
    "result": true
  },
  "described": {
    "use": ":original",
    "robot": "/image/describe",
    "provider": "aws",
    "format": "meta",
    "granularity": "list",
    "result": true
  },
  "filtered": {
    "result": true,
    "use": "described",
    "robot": "/file/filter",
    "accepts": [
      [
        "${file.meta.descriptions}",
        "includes",
        "Bridge"
      ]
    ]
  },
  "exported": {
    "use": "filtered",
    "robot": "/s3/store",
    "credentials": "YOUR_AWS_CREDENTIALS",
    "url_prefix": "https://demos.transloadit.com/"
  }
}' > ./steps.json

transloadify \
  --input "./prinsengracht.jpg" \
  --input "./chameleon.jpg" \
  --output "./output.example" \
  --steps "./steps.json"  
// composer require transloadit/php-sdk
use transloadit\Transloadit;
$transloadit = new Transloadit([
  "key" => "YOUR_TRANSLOADIT_KEY",
  "secret" => "YOUR_TRANSLOADIT_SECRET",
]);

// Add files to upload
$files = [];
array_push($files, "./prinsengracht.jpg")
array_push($files, "./chameleon.jpg")

// Start the Assembly
$response = $transloadit->createAssembly([
  "files" => $files, 
  "params" => [
    "steps" => [
      ":original" => [
        "robot" => "/upload/handle",
        "result" => true,
      ],
      "described" => [
        "use" => ":original",
        "robot" => "/image/describe",
        "result" => true,
        "format" => "meta",
        "granularity" => "list",
        "provider" => "aws",
      ],
      "filtered" => [
        "use" => "described",
        "robot" => "/file/filter",
        "result" => true,
        "accepts" => [
          "${file.meta.descriptions}" => "includes",
        ],
      ],
      "exported" => [
        "use" => "filtered",
        "robot" => "/s3/store",
        "credentials" => "YOUR_AWS_CREDENTIALS",
        "url_prefix" => "https://demos.transloadit.com/",
      ],
    ],
  ],
]);
# gem install transloadit
transloadit = Transloadit.new(
  :key => "YOUR_TRANSLOADIT_KEY",
  :secret => "YOUR_TRANSLOADIT_SECRET"
)

# Set Encoding Instructions
:original = transloadit.step ":original", "/upload/handle",
  :result => true
)
described = transloadit.step "described", "/image/describe",
  :use => ":original",
  :result => true,
  :format => "meta",
  :granularity => "list",
  :provider => "aws"
)
filtered = transloadit.step "filtered", "/file/filter",
  :use => "described",
  :result => true,
  :accepts => [["${file.meta.descriptions}","includes","Bridge"]]
)
exported = transloadit.step "exported", "/s3/store",
  :use => "filtered",
  :credentials => "YOUR_AWS_CREDENTIALS",
  :url_prefix => "https://demos.transloadit.com/"
)

assembly = transloadit.assembly(
  :steps => [ :original, described, filtered, exported ]
)

# Add files to upload
files = []
files.push("./prinsengracht.jpg")
files.push("./chameleon.jpg")

# Start the Assembly
response = assembly.create! *files

until response.finished?
  sleep 1; response.reload!
end

if !response.error?
  # handle success
end
# pip install pytransloadit
from transloadit import client

tl = client.Transloadit('YOUR_TRANSLOADIT_KEY', 'YOUR_TRANSLOADIT_SECRET')
assembly = tl.new_assembly()

# Set Encoding Instructions
assembly.add_step(':original', {
  'robot': '/upload/handle',
  'result': true
})
assembly.add_step('described', {
  'use': ':original',
  'robot': '/image/describe',
  'result': true,
  'format': 'meta',
  'granularity': 'list',
  'provider': 'aws'
})
assembly.add_step('filtered', {
  'use': 'described',
  'robot': '/file/filter',
  'result': true,
  'accepts': [['${file.meta.descriptions}','includes','Bridge']]
})
assembly.add_step('exported', {
  'use': 'filtered',
  'robot': '/s3/store',
  'credentials': 'YOUR_AWS_CREDENTIALS',
  'url_prefix': 'https://demos.transloadit.com/'
})
# Add files to upload
assembly.add_file(open('./prinsengracht.jpg', 'rb'))
assembly.add_file(open('./chameleon.jpg', 'rb'))

# Start the Assembly
assembly_response = assembly.create(retries=5, wait=True)

print assembly_response.data.get('assembly_id')

# or
print assembly_response.data['assembly_id']
// go get gopkg.in/transloadit/go-sdk.v1
package main

import (
	"fmt"

	"gopkg.in/transloadit/go-sdk.v1"
)

options := transloadit.DefaultConfig
options.AuthKey = "YOUR_TRANSLOADIT_KEY"
options.AuthSecret = "YOUR_TRANSLOADIT_SECRET"
client := transloadit.NewClient(options)

// Initialize new Assembly
assembly := transloadit.NewAssembly()

// Set Encoding Instructions
assembly.AddStep(":original", map[string]interface{}{
  "robot": "/upload/handle",
  "result": true
})
assembly.AddStep("described", map[string]interface{}{
  "use": ":original",
  "robot": "/image/describe",
  "result": true,
  "format": "meta",
  "granularity": "list",
  "provider": "aws"
})
assembly.AddStep("filtered", map[string]interface{}{
  "use": "described",
  "robot": "/file/filter",
  "result": true,
  "accepts": []interface{}{
    []interface{}{"${file.meta.descriptions}", "includes", "Bridge"},
  }
})
assembly.AddStep("exported", map[string]interface{}{
  "use": "filtered",
  "robot": "/s3/store",
  "credentials": "YOUR_AWS_CREDENTIALS",
  "url_prefix": "https://demos.transloadit.com/"
})

// Add files to upload
assembly.AddFile("myfile_1", "./prinsengracht.jpg")
assembly.AddFile("myfile_2", "./chameleon.jpg")

// Start the Assembly
info, err := client.StartAssembly(context.Background(), assembly)
if err != nil {
  panic(err)
}

// All files have now been uploaded and the Assembly has started but no
// results are available yet since the conversion has not finished.
// WaitForAssembly provides functionality for polling until the Assembly
// has ended.
info, err = client.WaitForAssembly(context.Background(), info)
if err != nil {
  panic(err)
}

fmt.Printf("You can check some results at: \n")
fmt.Printf("  - %s\n", info.Results[":original"][0].SSLURL)
fmt.Printf("  - %s\n", info.Results["described"][0].SSLURL)
fmt.Printf("  - %s\n", info.Results["filtered"][0].SSLURL)
fmt.Printf("  - %s\n", info.Results["exported"][0].SSLURL)

// compile 'com.transloadit.sdk:transloadit:0.1.5'
import com.transloadit.sdk.Assembly;
import com.transloadit.sdk.Transloadit;
import com.transloadit.sdk.exceptions.LocalOperationException;
import com.transloadit.sdk.exceptions.RequestException;
import com.transloadit.sdk.response.AssemblyResponse;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

public class Main {
  public static void main(String[] args) {
    Transloadit transloadit = new Transloadit("YOUR_TRANSLOADIT_KEY", "YOUR_TRANSLOADIT_SECRET");
    Assembly assembly = transloadit.newAssembly();
    // Set Encoding Instructions
    Map<String Object> originalStepOptions = new HashMap();
    originalStepOptions.put("result", true);
    assembly.addStep(":original", "/upload/handle", originalStepOptions);
      
    Map<String Object> describedStepOptions = new HashMap();
    describedStepOptions.put("use", ":original");
    describedStepOptions.put("result", true);
    describedStepOptions.put("format", "meta");
    describedStepOptions.put("granularity", "list");
    describedStepOptions.put("provider", "aws");
    assembly.addStep("described", "/image/describe", describedStepOptions);
      
    Map<String Object> filteredStepOptions = new HashMap();
    filteredStepOptions.put("use", "described");
    filteredStepOptions.put("result", true);
    filteredStepOptions.put("accepts", new String[]{"["${file.meta.descriptions}", "includes", "Bridge"]"});
    assembly.addStep("filtered", "/file/filter", filteredStepOptions);
      
    Map<String Object> exportedStepOptions = new HashMap();
    exportedStepOptions.put("use", "filtered");
    exportedStepOptions.put("credentials", "YOUR_AWS_CREDENTIALS");
    exportedStepOptions.put("url_prefix", "https://demos.transloadit.com/");
    assembly.addStep("exported", "/s3/store", exportedStepOptions);
      
    // Add files to upload
    assembly.addFile(new File("./prinsengracht.jpg"));
    assembly.addFile(new File("./chameleon.jpg"));
    
    // Start the Assembly
    try {
      AssemblyResponse response = assembly.save();
      
      // Wait for Assembly to finish executing
      while (!response.isFinished()) {
        response = transloadit.getAssemblyByUrl(response.getSslUrl());
      }
      
      System.out.println(response.getId());
      System.out.println(response.getUrl());
      System.out.println(response.json());
    } catch (RequestException | LocalOperationException e) {
      // Handle exception here
    }
  }
}

We're uploading two photos, one of which contains bridges:

When I check the meta.descriptions of the results of the described Step, I'll see:

[ 'Water',
  'Outdoors',
  'Bridge',
  'Building',
  'Canal',
  'Castle',
  'Architecture',
  'Fort' ]
https://demos.transloadit.com/53/fae6219071430cb7b794cf9f3513c2/prinsengracht.jpg

Only photos with 'Bridge' are allowed through, so the photo of our chameleon would not have been saved on S3 in the proper location either.

In such cases you could choose to:

  • gracefully ignore
  • error out hard
  • pipe unrecognized images to an export Step that uses a different directory, like ./flagged-for-review/

Here are more related AI demos with code samples for all major platforms:

Docs

Since these Robots remain in Tech Preview for now, we could still change the implementation, but we've already written preliminary documentation:

Have fun!

We're happy to expand this post, our docs, and how the bots work, based on your feedback. Just leave a comment below or on Twitter.