Added celebrity detection

This commit is contained in:
Hugo Bernier 2020-08-12 23:26:54 -04:00
parent c06dce146b
commit d3eb04fd59
9 changed files with 82 additions and 154 deletions

View File

@ -2487,11 +2487,6 @@
"isomorphic-fetch": "^2.2.1"
}
},
"@microsoft/microsoft-graph-types": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-1.13.0.tgz",
"integrity": "sha512-U55VWJWwVxgGZSSJy+s6UtExK6FNnGxhIqE6MkEOqjRwge40QTqBDhNlrCLJzXXDs5Cl0EnDkcgNDMjL97gd5A=="
},
"@microsoft/node-core-library": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@microsoft/node-core-library/-/node-core-library-3.13.0.tgz",
@ -4264,51 +4259,6 @@
}
}
},
"@pnp/graph": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@pnp/graph/-/graph-2.0.8.tgz",
"integrity": "sha512-kLL0Zvu4jvr88voRgCv//ZpU0k3qx/e1sfI1dXHGuo2yp1BlUpYTcJj1ZW3y6C3Zu2lCfIC5F83zcQ9JgoZ+DQ==",
"requires": {
"@microsoft/microsoft-graph-types": "1.13.0",
"@pnp/common": "2.0.8",
"@pnp/logging": "2.0.8",
"@pnp/odata": "2.0.8",
"tslib": "2.0.0"
},
"dependencies": {
"@pnp/common": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@pnp/common/-/common-2.0.8.tgz",
"integrity": "sha512-z3un/uLnmBHCk6y+OxK9CioI2XqFWw8m6GLdSpkXUcoG7Is41Rqia89Sy6CatAVAhhAcLoW9brI884ju/OXhCA==",
"requires": {
"tslib": "2.0.0"
}
},
"@pnp/logging": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@pnp/logging/-/logging-2.0.8.tgz",
"integrity": "sha512-76I5j9g/2CgxtscujPvx5vbm3OifuDJgjUYZ3hkuMSmFwK2dN7WcLA+gQqiEkLcSgPx1M9G7vdrZARQSc2pUNA==",
"requires": {
"tslib": "2.0.0"
}
},
"@pnp/odata": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@pnp/odata/-/odata-2.0.8.tgz",
"integrity": "sha512-keHdsXIiRd6cd5xKQ2hA/RLfYCCm56um5CvWAFts0xdFjsxaLlSkFgvHaw0u2lF7+4Jyr6vvmjCAOlUuPwIZhg==",
"requires": {
"@pnp/common": "2.0.8",
"@pnp/logging": "2.0.8",
"tslib": "2.0.0"
}
},
"tslib": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
}
}
},
"@pnp/logging": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/@pnp/logging/-/logging-1.3.8.tgz",
@ -4339,50 +4289,6 @@
}
}
},
"@pnp/sp": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@pnp/sp/-/sp-2.0.8.tgz",
"integrity": "sha512-/+V3IilrOI2ab2LOHOjruQhHPAjMbtWaUNEziDSi2eFUGc+8joCtrudM6MN14z6yMGyAItW2AyyBMqaaaceH2Q==",
"requires": {
"@pnp/common": "2.0.8",
"@pnp/logging": "2.0.8",
"@pnp/odata": "2.0.8",
"tslib": "2.0.0"
},
"dependencies": {
"@pnp/common": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@pnp/common/-/common-2.0.8.tgz",
"integrity": "sha512-z3un/uLnmBHCk6y+OxK9CioI2XqFWw8m6GLdSpkXUcoG7Is41Rqia89Sy6CatAVAhhAcLoW9brI884ju/OXhCA==",
"requires": {
"tslib": "2.0.0"
}
},
"@pnp/logging": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@pnp/logging/-/logging-2.0.8.tgz",
"integrity": "sha512-76I5j9g/2CgxtscujPvx5vbm3OifuDJgjUYZ3hkuMSmFwK2dN7WcLA+gQqiEkLcSgPx1M9G7vdrZARQSc2pUNA==",
"requires": {
"tslib": "2.0.0"
}
},
"@pnp/odata": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@pnp/odata/-/odata-2.0.8.tgz",
"integrity": "sha512-keHdsXIiRd6cd5xKQ2hA/RLfYCCm56um5CvWAFts0xdFjsxaLlSkFgvHaw0u2lF7+4Jyr6vvmjCAOlUuPwIZhg==",
"requires": {
"@pnp/common": "2.0.8",
"@pnp/logging": "2.0.8",
"tslib": "2.0.0"
}
},
"tslib": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
}
}
},
"@pnp/sp-clientsvc": {
"version": "1.3.11",
"resolved": "https://registry.npmjs.org/@pnp/sp-clientsvc/-/sp-clientsvc-1.3.11.tgz",

View File

@ -20,8 +20,6 @@
"@microsoft/sp-office-ui-fabric-core": "1.11.0",
"@microsoft/sp-property-pane": "1.11.0",
"@microsoft/sp-webpart-base": "1.11.0",
"@pnp/graph": "^2.0.8",
"@pnp/sp": "^2.0.8",
"@pnp/spfx-controls-react": "^1.19.0",
"@pnp/spfx-property-controls": "^1.20.0-beta.1472053",
"cropperjs": "^1.5.6",

View File

@ -23,6 +23,7 @@ export class AnalysisService implements IAnalysisService {
new ApiKeyCredentials({ inHeader: { 'Ocp-Apim-Subscription-Key': this.key } }), this.endpoint);
var analysis: AnalyzeImageInStreamResponse = (await computerVisionClient.analyzeImageInStream(buf, {
details: ["Celebrities"],
visualFeatures: ["Categories",
"Adult",
"Tags",

View File

@ -3,28 +3,31 @@ import * as React from 'react';
import { IAnalysisDialogContentProps } from './IAnalysisDialogContentProps';
import { IAnalysisDialogContentState } from './IAnalysisDialogContentState';
import styles from './AnalysisDialogContent.module.scss';
import { css } from "@uifabric/utilities/lib/css";
// Used for localized text
import * as strings from 'ProfilePhotoEditorWebPartStrings';
import { Text } from '@microsoft/sp-core-library';
// Used to determine if we should be making real calls to APIs or just mock calls
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
// Stuff we use for the dialog
import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import {
MessageBar,
MessageBarType
} from 'office-ui-fabric-react';
import { Image, ImageFit } from 'office-ui-fabric-react/lib/Image';
import { Panel } from 'office-ui-fabric-react/lib/Panel';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
import styles from './AnalysisDialogContent.module.scss';
// Used for localized text
import * as strings from 'ProfilePhotoEditorWebPartStrings';
// Used to determine if we should be making real calls to APIs or just mock calls
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { css } from "@uifabric/utilities/lib/css";
import {
MessageBar,
MessageBarType
} from 'office-ui-fabric-react';
import { Image, ImageFit } from 'office-ui-fabric-react/lib/Image';
// Stuff we use for analysis results
import { IAnalysisService, AnalysisService, MockAnalysisService } from '../../../../services/AnalysisServices';
import { AnalyzeImageInStreamResponse, ImageTag } from '@azure/cognitiveservices-computervision/esm/models';
import AnalysisChecklist from '../AnalysisChecklist/AnalysisChecklist';
@ -32,16 +35,9 @@ import AnalysisChecklist from '../AnalysisChecklist/AnalysisChecklist';
// This is used if you use the graph client to update pictures
import { MSGraphClient } from '@microsoft/sp-http';
// This is used if you use the PnP library to update pictures
import { sp } from "@pnp/sp";
import "@pnp/sp/profiles";
export class AnalysisDialogContent extends
React.Component<IAnalysisDialogContentProps, IAnalysisDialogContentState> {
/**
*
*/
constructor(props: IAnalysisDialogContentProps) {
super(props);
this.state = {
@ -74,11 +70,23 @@ export class AnalysisDialogContent extends
const analysis: AnalyzeImageInStreamResponse = await service.AnalyzeImage(this.props.imageUrl);
// Evaluate analysis against requirements
// Is this a portrait?
const isPortrait: boolean = analysis && analysis.categories && analysis.categories.filter(c => c.name === "people_portrait").length > 0;
// If the portrait valid?
const isPortraitValid: boolean = photoRequirements.requirePortrait ? isPortrait : true;
// is there only one person in the photo?
const onlyOnePersonValid: boolean = analysis.faces.length === 1;
// Is this a clipart?
const isClipartValid: boolean = photoRequirements.allowClipart ? true : analysis.imageType.clipArtType === 0;
// Is this a line drawing?
const isLinedrawingValid: boolean = photoRequirements.allowLinedrawing ? true : analysis.imageType.lineDrawingType === 0;
// Are we looking at naughty pictures?
const isAdultValid: boolean = photoRequirements.allowAdult ? true : !analysis.adult.isAdultContent;
const isRacyValid: boolean = photoRequirements.allowRacy ? true : !analysis.adult.isRacyContent;
const isGoryValid: boolean = photoRequirements.allowGory ? true : !analysis.adult.isGoryContent;
@ -95,9 +103,16 @@ export class AnalysisDialogContent extends
});
}
// Did we find forbidden keywords
const keywordsValid: boolean = invalidKeywords.length < 1;
console.log("Invalid keywords", invalidKeywords);
// Look for celebrities
let celebName: string = undefined;
const categories = analysis && analysis.categories && analysis.categories.filter(c => c.detail !== undefined && c.detail.celebrities !== undefined);
if (categories && categories.length > 0) {
// Get the first celebrity
celebName = categories[0] && categories[0].detail && categories[0].detail.celebrities[0] && categories[0].detail.celebrities[0].name;
}
// Photo is valid if it meets all requirements
const isValid: boolean = isPortraitValid
@ -109,6 +124,7 @@ export class AnalysisDialogContent extends
&& isGoryValid
&& keywordsValid;
// Set the state so we can refresh the status
this.setState({
isAnalyzing: false,
analysis,
@ -122,7 +138,8 @@ export class AnalysisDialogContent extends
isRacyValid,
isGoryValid,
keywordsValid,
invalidKeywords
invalidKeywords,
celebrity: celebName
});
}
@ -142,7 +159,8 @@ export class AnalysisDialogContent extends
isLinedrawingValid,
onlyOnePersonValid,
invalidKeywords,
keywordsValid } = this.state;
keywordsValid,
celebrity } = this.state;
if (analysis !== undefined) {
console.log("Analysis", analysis);
@ -191,6 +209,11 @@ export class AnalysisDialogContent extends
<div className={styles.iconContainer} ><Icon iconName={isValid ? "CheckMark" : "StatusCircleErrorX"} className={css(styles.icon, isValid ? styles.iconGood : styles.iconBad)} /></div>
}
{!isAnalyzing && celebrity !== undefined &&
<div><p>{Text.format(strings.YouLookLikeACelebrity, celebrity)}</p></div>
}
{!isAnalyzing && isValid &&
<div>{strings.AnalysisGoodLabel}</div>
}
@ -244,33 +267,35 @@ export class AnalysisDialogContent extends
}
private onUpdateProfilePhoto = async (_ev?: React.SyntheticEvent<HTMLElement, Event>) => {
console.log("Submitting photo");
// Get image array buffer
const profileBlob: Blob = this.props.blob;
// Submit using the approach you want
//this.updateProfilePicUsingGraph(profileBlob);
this.updateProfilePicUsingPnP(profileBlob);
this.updateProfilePicUsingGraph(profileBlob);
//this.updateProfilePicUsingPnP(profileBlob);
}
private async updateProfilePicUsingPnP(blob: Blob) {
console.log("Update profile pic using PnP", blob);
const response = await sp.profiles.setMyProfilePic(blob);
console.log("Profile property Updated", response);
this.setState({
isSubmitted: true
});
}
// private async updateProfilePicUsingPnP(blob: Blob) {
// pnpSetup({
// spfxContext: this.props.context
// });
// console.log("Update profile pic using PnP", blob);
// const response = await sp.profiles.setMyProfilePic(blob);
// console.log("Profile property Updated", response);
// this.setState({
// isSubmitted: true
// });
// }
// Update photo using Graph.
// See https://docs.microsoft.com/en-us/graph/api/profilephoto-update?view=graph-rest-1.0&tabs=http
private async updateProfilePicUsingGraph(blob: Blob) {
console.log("Update profile pic using Graph", blob);
this.props.context.msGraphClientFactory
.getClient().then((client: MSGraphClient) => {
client
.api("me/photo/$value")
.version("v1.0").header("Content-Type", blob.type).put(blob, (error, res) => {
.version("v1.0").header("Content-Type", blob.type).put(blob, (error, _res) => {
if (error) {
console.log("Error updating profile", error);
} else {
@ -286,15 +311,4 @@ export class AnalysisDialogContent extends
private onDismiss = (_ev?: React.SyntheticEvent<HTMLElement, Event>) => {
this.props.onDismiss();
}
// private dataURLtoBlob = (dataurl: string): Blob => {
// var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
// bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
// while (n--) {
// u8arr[n] = bstr.charCodeAt(n);
// }
// return new Blob([u8arr], { type: mime });
// }
}

View File

@ -16,4 +16,5 @@ export interface IAnalysisDialogContentState {
keywordsValid?: boolean;
invalidKeywords?: string[];
isSubmitted: boolean;
celebrity?: string;
}

View File

@ -289,8 +289,6 @@ export default class ProfilePhotoEditor extends React.Component<IProfilePhotoEdi
// Get the image to approve
const imageToApprove: string = this.cropper.getCroppedCanvas().toDataURL();
this.cropper.getCroppedCanvas().toBlob((blob: Blob)=> {
console.log("Blob", blob);
const photoRequirements: IPhotoRequirements = {
allowAdult: this.props.allowAdult,
allowClipart: this.props.allowClipart,

View File

@ -10,6 +10,9 @@ import styles from './WebCamDialog.module.scss';
import Webcam from "react-webcam";
/**
* Set the video constraints to be a square from the user-facing camera
*/
const videoConstraints = {
width: 300,
height: 300,
@ -41,8 +44,6 @@ export class WebCamDialog extends React.Component<IWebCamDialogProps, IWebCamDia
screenshotFormat="image/jpeg"
videoConstraints={videoConstraints}
imageSmoothing={true}
onUserMedia={() => console.log("OnUserMedia")}
onUserMediaError={() => console.log("OnUserMediaError")}
screenshotQuality={0.92}
/>
@ -54,12 +55,19 @@ export class WebCamDialog extends React.Component<IWebCamDialogProps, IWebCamDia
);
}
/**
* Captures an image from the web cam
*/
private onCapture = () => {
const imageSrc = this.webcamRef.getScreenshot();
console.log("ImageSrc", imageSrc);
this.props.onCapture(imageSrc);
}
/**
*
* Dismisses the dialog
* @param _ev
*/
private onDismiss = (_ev?: React.SyntheticEvent<HTMLElement, Event>) => {
this.props.onDismiss();
}

View File

@ -1,5 +1,6 @@
define([], function() {
return {
YouLookLikeACelebrity: "Uh... did anyone ever tell you that you look like {0}? It's uncanny!",
CaptureButtonLabel: "Capture",
WebCamDialogTitle: "Insert photo from camera",
NoKeywords: "(none)",

View File

@ -1,4 +1,5 @@
declare interface IProfilePhotoEditorWebPartStrings {
YouLookLikeACelebrity: string;
CaptureButtonLabel: string;
WebCamDialogTitle: string;
NoKeywords: string;