如何使用 AWS Amplify 为 React Native 应用添加 AI 指南
应用案例:从图像中识别文本
结论
AWS Amplify 发展迅速。它是一个框架,您可以像订购披萨一样轻松地在几分钟内配置云后端基础设施。AWS Amplify 交付基础设施的速度甚至比披萨送达的速度还要快。
它提供种类繁多的服务,例如身份验证、存储、API、数据存储等等。我已经写过几篇关于 AWS Amplify 的博客文章,例如地理位置搜索和推送通知,但现在是时候谈谈 AWS Amplify 和人工智能/机器学习了。
如果你关注人工智能,你会发现构建、测试和部署自定义模型是完全可行的。例如,你可以使用 SageMaker(但它并未与 AWS Amplify 集成)。AWS Amplify 中也预置了一些模型,例如文本翻译、文本转语音、图像文本识别等等。
应用案例:从图像中识别文本
本指南将展示如何使用 React Native 应用。已认证的用户可以使用摄像头拍照,并将照片中的文本与数据库中的文本进行匹配。您可以将电话号码或电子邮件地址与数据库中的文本字符串关联起来,并根据匹配结果执行其他操作,例如发送电子邮件。
入门
我将使用 NPM,当然你也可以使用 Yarn。
设置 React Native
首先,我们将创建我们将要使用的 React Native 应用程序。
$ npx expo init ScanApp
> Choose a template: **Tabs**
$ cd ScanApp
expo install expo-camera
$ npm install aws-amplify aws-amplify-react-native
设置 AWS Amplify
首先,我们需要安装 AWS Amplify CLI。Amplify CLI 是一个命令行工具,可用于创建和部署各种 AWS 服务。
要安装 CLI,我们将运行以下命令:
$ npm install -g @aws-amplify/cli
接下来,我们将使用 AWS 账户中的用户配置 CLI:
$ amplify configure
如需观看 CLI 配置过程的视频演示,请点击
现在我们可以从 React Native 应用的根目录初始化一个新的 Amplify 项目:
$ amplify init
接下来我们将按照以下步骤进行操作:
- 请输入项目名称: amplifyAIapp (或您喜欢的项目名称)
- 请输入环境名称: dev (请使用此名称,因为我们将引用它)
- 选择默认编辑器: Visual Studio Code (或您的文本编辑器)
- 选择您要构建的应用类型: JavaScript
- 你使用的是哪个 JavaScript 框架: react-native
- 源目录路径: /
- 分发目录路径: 构建
- 构建命令: npm run-script build
- 启动命令: npm run-script start
- 您想使用 AWS 配置文件吗? 是
- 请选择您要使用的配置文件: 您的用户配置文件
- 现在,我们的 Amplify 项目已经创建完成,我们可以继续进行下一步了。
将 GraphQL 添加到您的项目中
您的 React Native 应用已启动并运行,AWS Amplify 也已配置完成。Amplify 提供多种服务,可用于丰富您的应用功能。接下来,我们来添加一个 API。
Amplify add api
以下步骤将依次进行:
- 选择GraphQL
- 请输入 API 名称:aiAPI(您希望的 API 名称)
- 选择 API 的授权类型:Amazon Cognito 用户池(因为我们只将此应用用于已认证用户,但您可以选择其他选项)
- 请选择是否使用默认身份验证和安全配置:默认配置
- 您希望用户如何登录?用户名(同时启用 AWS Amplify Auth 模块)
- 您想配置高级设置吗?不用了,我完成了。
- 您是否拥有带注释的 GraphQL schema ?
- 您是否需要引导式模式创建?:否
- 提供自定义类型名称:用户
您的 API 和模式定义已创建完成。您可以在项目目录中找到它们:
Amplify > backend > api > name of your api
打开 schema.graphql 文件,并将代码替换为以下代码:
type text @model {
id: ID!
text: String!
email: String!
}
@model 指令会为您创建一个 DynamoDB 实例。还有更多指令可用,完整指令集请参阅AWS Amplify 文档。
现在我们先将配置推送到云端,以便同时创建数据库:
amplify push
- 你确定要继续吗?是
- 您是否要为新建的 GraphQL API 生成代码?是
- 选择代码生成目标语言:JavaScript
- 请输入 GraphQL 查询、变更和订阅的文件名模式(默认)。是否要生成/更新所有可能的 GraphQL 操作(查询、变更和订阅)?是
- 输入最大语句深度(如果您的模式嵌套很深,请增加默认值)(默认值为 2)
在您的应用中添加预测功能
我们将首先按照以下步骤为您的应用添加图像文本识别功能:
Amplify add predictions
- 选择识别
- 选择识别文本
- 给你的功能命名,然后按回车键。
- 您还要鉴定文件吗?否
- 谁应该有访问权限?仅限已认证用户。
向您的应用添加 S3 存储空间
我们将把照片保存到 S3 存储桶中。我们将创建该存储桶。
amplify add storage
- 选择内容(图片、音频、视频等)
- 请为您的资源提供一个易于理解的名称,该名称将用于在项目中标记此类别:
- 请提供存储桶名称:
- 访问权限对象:仅限已认证用户
- 选择创建/更新、读取和删除
- 您是否要为 S3 存储桶添加 Lambda 触发器?否
向项目中添加一个函数
通过添加一个函数,我们将创建一个 Lambda 函数。该 Lambda 函数的目标是接收照片中的文本,并将其与数据库中的数据进行匹配。
Amplify add function
请按照以下步骤操作:
- 请为您的资源提供一个易于理解的名称,该名称将用作项目中此类别的标签:matchFunction
- 请提供 AWS Lambda 函数名称:
- 选择要使用的函数模板:Hello world 函数
- 您是否希望从 Lambda 函数访问此项目中创建的其他资源?是
- 选择API
- 请选择要允许 scanapp 执行的操作?读取
- 您现在要编辑本地 lambda 函数吗?否
您的函数已创建,您可以在项目目录中找到它:
Amplify > backend > function > name of your function
进入matchFunction 的src 目录并安装此软件包
$ npm install aws-sdk
打开 matchFunction-cloudformation-template.json 文件,并将以下代码添加到 lambdaexecutionpolicy 部分的 statement 数组中。此代码将使您的 Lambda 函数能够访问您的 DynamoDB 资源:
{
"Effect": "Allow",
"Action": [
"dynamodb:Get*",
"dynamodb:Query",
"dynamodb:Scan"
],
"Resource": {
"Fn::Sub": [
"arn:aws:dynamodb:${region}:${account}:table/*",
{
"region": {
"Ref": "AWS::Region"
},
"account": {
"Ref": "AWS::AccountId"
},
}
]
}
}
打开 src/app.js 文件并粘贴以下代码。
/* Amplify Params - DO NOT EDIT
You can access the following resource attributes as environment variables from your Lambda function
var environment = process.env.ENV
var region = process.env.REGION
var apiScanappGraphQLAPIIdOutput = process.env.API_SCANAPP_GRAPHQLAPIIDOUTPUT
var apiScanappGraphQLAPIEndpointOutput = process.env.API_SCANAPP_GRAPHQLAPIENDPOINTOUTPUT
Amplify Params - DO NOT EDIT */ const AWS = require("aws-sdk");
AWS.config.region = process.env.REGION;
const dynamodb = new AWS.DynamoDB.DocumentClient();
exports.handler = async (event, context, callback) => {
try {
let renderItems = await getItems();
const text = event.arguments.input.text.toLowerCase();
let foundItem = "";
for (let i = 0, iMax = renderItems.length; i < iMax; i++) {
if (text.includes(renderItems[i].text.toLowerCase())) {
foundItem = renderItems[i];
break;
}
}
const response = {
items: JSON.stringify(foundItem)
};
callback(null, response);
} catch (error) {
callback(error);
}
};
function getItems() {
let tableName = "text";
if (process.env.ENV && process.env.ENV !== "NONE") {
tableName =
tableName +
"-" +
process.env.API_SCANAPP_GRAPHQLAPIIDOUTPUT +
"-" +
process.env.ENV;
}
let scanParams = {
TableName: tableName
};
return new Promise((resolve, reject) => {
dynamodb.scan(scanParams, (err, data) => {
if (err) {
console.log("err", err);
reject(err);
} else {
console.log("Query succeeded.");
resolve(data.Items);
}
});
});
}
我们将再次将其推送到云端,然后就可以构建我们的应用程序了。但在此之前,我们还需要更新 API 架构,请转到:
Amplify > backend > api > name of your api
打开 schema.graphql 文件,并将代码替换为以下代码:
type text @model {
id: ID!
text: String!
email: String!
}
type Mutation {
match(input: matchInput): matchResult @function(name: "matchFunction-${env}")
}
type matchResult {
items: String
}
input matchInput {
text: String!
}
现在将其推送到 AWS
amplify push
通过 Cognito 和 AppSync 添加一些数据
在AWS Cognito控制台中,点击“管理器用户池”>“用户和组”>“创建用户”。填写表单,并勾选所有复选框。点击新创建的用户,记下子值(例如 b14cc22-c73f-4775-afd7-b54f222q4758),然后在菜单中选择“应用程序客户端” ,记下客户端 Web 页面(顶部)中的应用程序客户端 ID 。在下一步中使用这些值。
让我们添加一些您可以在应用中使用的数据。请转到控制台中的 AppSync 服务。
- 通过控制台访问AWS AppSync 。
- 打开你的项目
- 点击“查询”。
- 点击“通过 Cognito 用户池登录”按钮,使用 Cognito 用户登录(您可以通过 Cognito 控制台或应用程序创建用户)(使用您已记录的数据)
- 添加以下代码并运行代码(请将文本替换为您要拍摄照片的图片中的文本,并将您的电子邮件地址替换为图片中的文本):
mutation createText {
createText( input: {
text: "<TEXT>",
email: "<EMAILADDRESS>"
}
){
id
text
email
}
}
- 用其他值运行这段代码几次,这样你就能看出它实际上只匹配你的文本。
让我们来构建 React Native 应用
我开发了一个应用,用户需要登录后立即加载摄像头。通过这个摄像头,用户可以拍摄照片。照片会被上传到 S3,然后从中提取文本,发送到 lambda 函数进行匹配。
正如你所知,我不会在应用的UX设计上花费太多时间,我只想展示它的设置有多么简单。至于UX设计,就由你来决定了 :)
您可以从以下链接下载该项目:https://github.com/rpostulart/Aiapp
- 或者 -
进入项目根目录,打开 App.js 文件,并将其替换为以下代码:
import * as React from "react";
import { Platform, StatusBar, StyleSheet, View } from "react-native";
import { SplashScreen } from "expo";
import * as Font from "expo-font";
import { Ionicons } from "@expo/vector-icons";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import AppNavigator from "./navigation/AppNavigator";
import useLinking from "./navigation/useLinking";
const Stack = createStackNavigator();
export default function App(props) {
const [isLoadingComplete, setLoadingComplete] = React.useState(false);
const [initialNavigationState, setInitialNavigationState] = React.useState();
const containerRef = React.useRef();
const { getInitialState } = useLinking(containerRef);
// Load any resources or data that we need prior to rendering the app
React.useEffect(() => {
async function loadResourcesAndDataAsync() {
try {
SplashScreen.preventAutoHide();
// Load our initial navigation state
setInitialNavigationState(await getInitialState());
// Load fonts
await Font.loadAsync({
...Ionicons.font,
"space-mono": require("./assets/fonts/SpaceMono-Regular.ttf")
});
} catch (e) {
// We might want to provide this error information to an error reporting service
console.warn(e);
} finally {
setLoadingComplete(true);
SplashScreen.hide();
}
}
loadResourcesAndDataAsync();
}, []);
if (!isLoadingComplete && !props.skipLoadingScreen) {
return null;
} else {
return (
<View style={styles.container}>
{Platform.OS === "ios" && <StatusBar barStyle="default" />}
<NavigationContainer
ref={containerRef}
initialState={initialNavigationState}
>
<Stack.Navigator>
<Stack.Screen name="Root" component={AppNavigator} />
</Stack.Navigator>
</NavigationContainer>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff"
}
});
进入 screens 目录并添加这些文件
HomeScreen.js
import React from "react";
import { Alert, StyleSheet, Text, View, TouchableOpacity } from "react-native";
import Constants from "expo-constants";
import { Camera } from "expo-camera";
import * as Permissions from "expo-permissions";
import Amplify, {
API,
Storage,
Predictions,
graphqlOperation
} from "aws-amplify";
import { AmazonAIPredictionsProvider } from "@aws-amplify/predictions";
import * as mutations from "../src/graphql/mutations";
import awsconfig from "../aws-exports";
Amplify.configure(awsconfig);
Amplify.addPluggable(new AmazonAIPredictionsProvider());
import { Ionicons } from "@expo/vector-icons";
export default class CameraScreen extends React.Component {
state = {
flash: "off",
zoom: 0,
autoFocus: "on",
type: "back",
whiteBalance: "auto",
ratio: "16:9",
newPhotos: false,
permissionsGranted: false,
pictureSize: "1280x720",
pictureSizes: ["1280x720"],
pictureSizeId: 0
};
async componentDidMount() {
const { status } = await Permissions.askAsync(Permissions.CAMERA);
this.setState({ permissionsGranted: status === "granted" });
}
toggleFocus = () =>
this.setState({ autoFocus: this.state.autoFocus === "on" ? "off" : "on" });
takePicture = () => {
if (this.camera) {
this.camera.takePictureAsync({ onPictureSaved: this.onPictureSaved });
}
};
uploadToStorage = async pathToImageFile => {
try {
const response = await fetch(pathToImageFile);
const blob = await response.blob();
const s3photo = await Storage.put("file-" + Date.now() + ".jpeg", blob, {
contentType: "image/jpeg"
});
await Predictions.identify({
text: {
source: {
key: s3photo.key,
level: "public" //optional, default is the configured on Storage category
},
format: "PLAIN" // Available options "PLAIN", "FORM", "TABLE", "ALL"
}
})
.then(async ({ text: { fullText } }) => {
const input = {
text: fullText
};
await API.graphql(graphqlOperation(mutations.match, { input: input }))
.then(result => {
const item = JSON.parse(result.data.match.items);
if (typeof item.text === "undefined") {
Alert.alert(`There was no match!`);
} else {
Alert.alert(
`Whoohoo! There was a match with ${item.text} the email has been send!`
);
}
})
.catch(err => console.log(err));
})
.catch(err => console.log(err));
//
} catch (err) {
console.log(err);
}
};
handleMountError = ({ message }) => console.error(message);
onPictureSaved = async photo => {
this.uploadToStorage(photo.uri);
};
renderNoPermissions = () => (
<View style={styles.noPermissions}>
<Text style={{ color: "white" }}>
Camera permissions not granted - cannot open camera preview.
</Text>
</View>
);
renderTopBar = () => (
<View style={styles.topBar}>
<TouchableOpacity style={styles.toggleButton} onPress={this.toggleFocus}>
<Text
style={[
styles.autoFocusLabel,
{ color: this.state.autoFocus === "on" ? "white" : "#6b6b6b" }
]}
>
FOCUS
</Text>
</TouchableOpacity>
</View>
);
renderBottomBar = () => (
<View style={styles.bottomBar}>
<View style={{ flex: 0.4 }}>
<TouchableOpacity
onPress={this.takePicture}
style={{ alignSelf: "center" }}
>
<Ionicons name="ios-radio-button-on" size={70} color="white" />
</TouchableOpacity>
</View>
</View>
);
renderCamera = () => (
<View style={{ flex: 1 }}>
<Camera
ref={ref => {
this.camera = ref;
}}
style={styles.camera}
onCameraReady={this.collectPictureSizes}
type={this.state.type}
autoFocus={this.state.autoFocus}
zoom={this.state.zoom}
whiteBalance={this.state.whiteBalance}
ratio={this.state.ratio}
pictureSize={this.state.pictureSize}
onMountError={this.handleMountError}
>
{this.renderTopBar()}
{this.renderBottomBar()}
</Camera>
</View>
);
render() {
const cameraScreenContent = this.state.permissionsGranted
? this.renderCamera()
: this.renderNoPermissions();
return <View style={styles.container}>{cameraScreenContent}</View>;
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#000"
},
camera: {
flex: 1,
justifyContent: "space-between"
},
topBar: {
flex: 0.2,
backgroundColor: "transparent",
flexDirection: "row",
justifyContent: "space-around",
paddingTop: Constants.statusBarHeight / 2
},
bottomBar: {
backgroundColor: "transparent",
alignSelf: "flex-end",
justifyContent: "space-between",
flex: 0.12,
flexDirection: "row"
},
noPermissions: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 10
},
toggleButton: {
flex: 0.25,
height: 40,
marginHorizontal: 2,
marginBottom: 10,
marginTop: 20,
padding: 5,
alignItems: "center",
justifyContent: "center"
},
autoFocusLabel: {
fontSize: 20,
fontWeight: "bold"
}
});
uploadToStorage 函数会将您的照片上传到 S3 存储桶,然后预测会识别文本,将其发送到 lambda 函数,并从 DynamoDB 中接收匹配结果。
进入导航目录并添加或更新以下文件:
AppNavigator.js
import * as React from "react";
import { createStackNavigator } from "@react-navigation/stack";
import { Auth } from "aws-amplify";
import HomeScreen from "../screens/HomeScreen";
import LoginScreen from "../screens/Login";
const Stack = createStackNavigator();
export default class Navigator extends React.Component {
//export default async function AppNavigator({ navigation, route }) {
// Set the header title on the parent stack navigator depending on the
// currently active tab. Learn more in the documentation:
// https://reactnavigation.org/docs/en/screen-options-resolution.html
state = {
user: "not authenticated"
};
async componentDidMount() {
await Auth.currentAuthenticatedUser({
bypassCache: true // Optional, By default is false. If set to true, this call will send a request to Cognito to get the latest user data
})
.then(async user => {
this.setState({ user: user });
})
.catch(err => {
// Is NOT logged in
console.log(err);
});
}
render() {
const user = this.state.user;
return (
<Stack.Navigator>
{user === "not authenticated" ? (
// No token found, user isn't signed in
<Stack.Screen
name="SignIn"
component={LoginScreen}
options={{
title: "Sign in"
// When logging out, a pop animation feels intuitive
}}
/>
) : (
// User is signed in
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
);
}
}
BottomTabNavigator.js
import * as React from "react";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import TabBarIcon from "../components/TabBarIcon";
import HomeScreen from "../screens/HomeScreen";
const BottomTab = createBottomTabNavigator();
const INITIAL_ROUTE_NAME = "Home";
export default function BottomTabNavigator({ navigation, route }) {
// Set the header title on the parent stack navigator depending on the
// currently active tab. Learn more in the documentation:
// https://reactnavigation.org/docs/en/screen-options-resolution.html
return (
<BottomTab.Navigator initialRouteName={INITIAL_ROUTE_NAME}>
<BottomTab.Screen
name="Home"
component={HomeScreen}
options={{
title: "Photo",
tabBarIcon: ({ focused }) => (
<TabBarIcon focused={focused} name="md-code-working" />
)
}}
/>
</BottomTab.Navigator>
);
}
metro.config.js
为确保 Metro 不会发生冲突,您需要将此文件添加到项目的根目录中。
module.exports = {
resolver: {
blacklistRE: /#current-cloud-backend\/.*/
},
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false
}
})
}
};
您的应用已准备就绪,您可以从根项目启动它:
> expo start
> press "i" (this will load the simulator)
使用您通过 AWS Cognito 创建的用户账号登录,拍照,稍等片刻……瞧!……找到了与您的文本匹配的记录。现在您可以使用返回对象中的电子邮件地址发送另一封邮件。我们暂不深入探讨此功能。
您可以在这里观看这部电影:
https://twitter.com/i/status/1230221875248279553
结论
人工智能是增强应用程序功能、为客户创造更多价值的下一个关键要素。我只用了两个晚上就搭建好了这款应用程序及其后端,这当然非常棒。想象一下,您可以多么迅速地为客户提供所需的功能。别再犹豫,现在就开始吧!同时,也开始探索其他预测功能。设置过程大致相同。
希望这篇指南对您有所帮助,期待您在评论区留下反馈。祝您编程愉快!
请查看 GitHub 获取实际代码:https://github.com/rpostulart/Aiapp
想及时了解最新博客动态或有任何疑问?请在推特上关注我。
也请阅读我的其他博客:
我即将撰写的下一篇博客是:
使用 React Native 和 AWS Amplify 为你的应用添加支付功能的指南。