feature: enable download JSON of trained model (#513)
* feature: enable dowload JSON of trained model * add manual tests Co-authored-by: kunzheng <58841788+kunzms@users.noreply.github.com>
This commit is contained in:
Родитель
529a0e819f
Коммит
4b85050bf0
|
@ -1,4 +1,28 @@
|
|||
# Test Runbook
|
||||
## **Feat: support download JSON for trained model**
|
||||
|
||||
> ### Feature description ###
|
||||
- Add a 'Download JSON file' button to train page
|
||||
|
||||
> ### Use Case ###
|
||||
|
||||
**As** a user
|
||||
**I want** to be able to download JSON file of my trained model
|
||||
**So** I can see raw JSON result of training
|
||||
|
||||
> ### Acceptance criteria ###
|
||||
|
||||
#### Scenario One ####
|
||||
**Given** I'm open new project, label, then train
|
||||
**When** I train a model
|
||||
**Then** I should be able to download JSON for that new trained model by clicking on 'Download JSON file' button
|
||||
|
||||
#### Scenario Two ####
|
||||
|
||||
**Given** I'm open existing project
|
||||
**When** I go to train page
|
||||
**Then** I should be able to download last train model by clicking on 'Download JSON file' button
|
||||
|
||||
|
||||
## **Feat: support region labeling**
|
||||
|
||||
|
|
|
@ -128,6 +128,8 @@ export const english: IAppStrings = {
|
|||
notTrainedYet: "Not trained yet",
|
||||
backEndNotAvailable: "Checkbox feature will work in future version of Form Recognizer service, please stay tuned.",
|
||||
addName: "Add a model name...",
|
||||
downloadJson: "Download JSON file",
|
||||
|
||||
},
|
||||
modelCompose: {
|
||||
title: "Model compose",
|
||||
|
|
|
@ -128,7 +128,8 @@ export const spanish: IAppStrings = {
|
|||
pleaseWait: "Por favor espera",
|
||||
notTrainedYet: "Aún no entrenado",
|
||||
backEndNotAvailable: "La función de casilla de verificación funcionará en la versión futura del servicio de reconocimiento de formularios, manténgase atento.",
|
||||
addName:"Agregar nombre de modelo ...",
|
||||
addName: "Agregar nombre de modelo ...",
|
||||
downloadJson: "Descargar archivo JSON",
|
||||
},
|
||||
modelCompose: {
|
||||
title: "Modelo componer",
|
||||
|
|
|
@ -128,6 +128,7 @@ export interface IAppStrings {
|
|||
notTrainedYet: string,
|
||||
backEndNotAvailable: string,
|
||||
addName: string,
|
||||
downloadJson: string;
|
||||
};
|
||||
modelCompose: {
|
||||
title: string,
|
||||
|
|
|
@ -38,7 +38,7 @@ export interface ITrainPageProps extends RouteComponentProps, React.Props<TrainP
|
|||
}
|
||||
|
||||
export interface ITrainPageState {
|
||||
inputedLabelFolderURL: string;
|
||||
inputtedLabelFolderURL: string;
|
||||
trainMessage: string;
|
||||
isTraining: boolean;
|
||||
currTrainRecord: ITrainRecordProps;
|
||||
|
@ -47,6 +47,8 @@ export interface ITrainPageState {
|
|||
trainingFailedMessage: string;
|
||||
hasCheckbox: boolean;
|
||||
modelName: string;
|
||||
modelUrl: string;
|
||||
currModelId: string;
|
||||
}
|
||||
|
||||
interface ITrainApiResponse {
|
||||
|
@ -81,7 +83,7 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
inputedLabelFolderURL: "",
|
||||
inputtedLabelFolderURL: "",
|
||||
trainMessage: strings.train.notTrainedYet,
|
||||
isTraining: false,
|
||||
currTrainRecord: null,
|
||||
|
@ -89,7 +91,9 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
showTrainingFailedWarning: false,
|
||||
trainingFailedMessage: "",
|
||||
hasCheckbox: false,
|
||||
modelName: ""
|
||||
modelName: "",
|
||||
modelUrl: "",
|
||||
currModelId: "",
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -104,6 +108,9 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
this.showCheckboxPreview(project);
|
||||
this.updateCurrTrainRecord(this.getProjectTrainRecord());
|
||||
}
|
||||
if (this.state.currTrainRecord) {
|
||||
this.setState({ currModelId: this.state.currTrainRecord.modelInfo.modelId });
|
||||
}
|
||||
this.appInsights = getAppInsights();
|
||||
document.title = strings.train.title + " - " + strings.appName;
|
||||
}
|
||||
|
@ -112,8 +119,8 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
const currTrainRecord = this.state.currTrainRecord;
|
||||
const localFileSystemProvider: boolean = this.props.project && this.props.project.sourceConnection &&
|
||||
this.props.project.sourceConnection.providerType === "localFileSystemProxy";
|
||||
const trainDisabled: boolean = localFileSystemProvider && (this.state.inputedLabelFolderURL.length === 0 ||
|
||||
this.state.inputedLabelFolderURL === strings.train.defaultLabelFolderURL);
|
||||
const trainDisabled: boolean = localFileSystemProvider && (this.state.inputtedLabelFolderURL.length === 0 ||
|
||||
this.state.inputtedLabelFolderURL === strings.train.defaultLabelFolderURL);
|
||||
|
||||
return (
|
||||
<div className="train-page skipToMainContent" id="pageTrain">
|
||||
|
@ -149,10 +156,10 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
<TextField
|
||||
className="label-folder-url-input"
|
||||
theme={getGreenWithWhiteBackgroundTheme()}
|
||||
onFocus={this.removeDefaultInputedLabelFolderURL}
|
||||
onChange={this.setInputedLabelFolderURL}
|
||||
onFocus={this.removeDefaultInputtedLabelFolderURL}
|
||||
onChange={this.setInputtedLabelFolderURL}
|
||||
placeholder={strings.train.defaultLabelFolderURL}
|
||||
value={this.state.inputedLabelFolderURL}
|
||||
value={this.state.inputtedLabelFolderURL}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
@ -198,11 +205,28 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
</div>
|
||||
<div className={!this.state.isTraining ? "" : "greyOut"}>
|
||||
{currTrainRecord &&
|
||||
<TrainPanel
|
||||
currTrainRecord={currTrainRecord}
|
||||
viewType={this.state.viewType}
|
||||
updateViewTypeCallback={this.handleViewTypeClick}
|
||||
<>
|
||||
<TrainPanel
|
||||
currTrainRecord={currTrainRecord}
|
||||
viewType={this.state.viewType}
|
||||
updateViewTypeCallback={this.handleViewTypeClick}
|
||||
/>
|
||||
<PrimaryButton
|
||||
ariaDescription={strings.train.downloadJson}
|
||||
style={{ "margin": "2rem auto" }}
|
||||
id="train-download-json_button"
|
||||
theme={getPrimaryGreenTheme()}
|
||||
autoFocus={true}
|
||||
className="flex-center"
|
||||
onClick={this.handleDownloadJSONClick}
|
||||
disabled={trainDisabled}>
|
||||
<FontIcon
|
||||
iconName="Download"
|
||||
style={{ fontWeight: 600 }}/>
|
||||
<h6 className="d-inline text-shadow-none ml-2 mb-0">
|
||||
{strings.train.downloadJson}</h6>
|
||||
</PrimaryButton>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -222,14 +246,14 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
);
|
||||
}
|
||||
|
||||
private removeDefaultInputedLabelFolderURL = () => {
|
||||
if (this.state.inputedLabelFolderURL === strings.train.defaultLabelFolderURL) {
|
||||
this.setState({inputedLabelFolderURL: ""});
|
||||
private removeDefaultInputtedLabelFolderURL = () => {
|
||||
if (this.state.inputtedLabelFolderURL === strings.train.defaultLabelFolderURL) {
|
||||
this.setState({inputtedLabelFolderURL: ""});
|
||||
}
|
||||
}
|
||||
|
||||
private setInputedLabelFolderURL = (event) => {
|
||||
this.setState({inputedLabelFolderURL: event.target.value});
|
||||
private setInputtedLabelFolderURL = (event) => {
|
||||
this.setState({inputtedLabelFolderURL: event.target.value});
|
||||
}
|
||||
|
||||
private onTextChanged = (ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, text: string) => {
|
||||
|
@ -294,7 +318,7 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
let trainPrefix;
|
||||
|
||||
if (this.props.project.sourceConnection.providerType === "localFileSystemProxy") {
|
||||
trainSourceURL = this.state.inputedLabelFolderURL;
|
||||
trainSourceURL = this.state.inputtedLabelFolderURL;
|
||||
trainPrefix = ""
|
||||
} else {
|
||||
trainSourceURL = provider.sas;
|
||||
|
@ -310,12 +334,14 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
modelName: this.state.modelName,
|
||||
};
|
||||
try {
|
||||
return await ServiceHelper.postWithAutoRetry(
|
||||
const result = await ServiceHelper.postWithAutoRetry(
|
||||
baseURL,
|
||||
payload,
|
||||
{},
|
||||
this.props.project.apiKey as string,
|
||||
);
|
||||
this.setState({modelUrl: result.headers.location});
|
||||
return result;
|
||||
} catch (err) {
|
||||
ServiceHelper.handleServiceError(err);
|
||||
}
|
||||
|
@ -323,7 +349,7 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
|
||||
private async getTrainStatus(operationLocation: string): Promise<any> {
|
||||
const timeoutPerFileInMs = 10000; // 10 second for each file
|
||||
const minimumTimeoutInMs = 300000; // 5 minutes minimum waiting time for each traingin process
|
||||
const minimumTimeoutInMs = 300000; // 5 minutes minimum waiting time for each training process
|
||||
const extendedTimeoutInMs = timeoutPerFileInMs * Object.keys(this.props.project.assets || []).length;
|
||||
const res = this.poll(() => {
|
||||
return ServiceHelper.getWithAutoRetry(
|
||||
|
@ -388,8 +414,8 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
}
|
||||
|
||||
/**
|
||||
* Poll function to repeatly check if request succeeded
|
||||
* @param func - function that will be called repeatly
|
||||
* Poll function to repeatably check if request succeeded
|
||||
* @param func - function that will be called repeatably
|
||||
* @param timeout - timeout
|
||||
* @param interval - interval
|
||||
*/
|
||||
|
@ -428,4 +454,47 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
private handleDownloadJSONClick = () => {
|
||||
this.triggerJsonDownload();
|
||||
}
|
||||
|
||||
private async triggerJsonDownload(): Promise<any> {
|
||||
const currModelUrl = this.props.project.apiUriBase + constants.apiModelsPath + "/" + this.state.currTrainRecord.modelInfo.modelId;
|
||||
const modelUrl = this.state.modelUrl.length ? this.state.modelUrl : currModelUrl;
|
||||
const modelJSON = await this.getModelsJson(this.props.project, modelUrl);
|
||||
|
||||
const fileURL = window.URL.createObjectURL(
|
||||
new Blob([modelJSON]));
|
||||
const fileLink = document.createElement("a");
|
||||
const fileBaseName = "model";
|
||||
const downloadFileName =`${fileBaseName}-${this.state.currTrainRecord.modelInfo.modelId}.json`;
|
||||
|
||||
fileLink.href = fileURL;
|
||||
fileLink.setAttribute("download", downloadFileName);
|
||||
document.body.appendChild(fileLink);
|
||||
fileLink.click();
|
||||
}
|
||||
|
||||
private async getModelsJson(project: IProject, modelUrl: string,) {
|
||||
const baseURL = url.resolve(
|
||||
project.apiUriBase,
|
||||
modelUrl)
|
||||
const config = {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"withCredentials": "true",
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
return await ServiceHelper.getWithAutoRetry(
|
||||
baseURL,
|
||||
config,
|
||||
project.apiKey as string,
|
||||
).then(res => res.request.response);
|
||||
} catch (error) {
|
||||
ServiceHelper.handleServiceError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче