This commit is contained in:
My Ho 2020-04-29 09:32:07 -07:00
Родитель c1afb7e355 347f609096
Коммит 94f09908a1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 8BDF9D76BB6159DC
49 изменённых файлов: 10883 добавлений и 82 удалений

13
.dependabot/config.yml Normal file
Просмотреть файл

@ -0,0 +1,13 @@
version: 1
update_configs:
target_branch: "develop"
# Keep package.json (& lockfiles) up to date weekly
- package_manager: "javascript"
directory: "/"
update_schedule: "weekly"
default_labels:
- "dependencies"
- "dependabot"

6
.gitignore поставляемый
Просмотреть файл

@ -19,6 +19,7 @@
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
@ -37,3 +38,8 @@ secrets.sh
# complexity reports
es6-src/
report/
# VoTT Server
server/lib
server/node_modules
server/coverage

Просмотреть файл

@ -28,6 +28,7 @@ VoTT helps facilitate an end-to-end machine learning pipeline:
<!-- toc -->
- [VoTT (Visual Object Tagging Tool)](#vott-visual-object-tagging-tool)
- [Table of Contents](#table-of-contents)
- [Getting Started](#getting-started)

Просмотреть файл

@ -17,7 +17,7 @@ jobs:
- job: MacOS
pool:
vmImage: macOS-10.13
vmImage: macOS-10.15
timeoutInMinutes: 60 # how long to run the job before automatically cancelling
steps:
- checkout: self # self represents the repo where the initial Pipelines YAML file was found
@ -26,7 +26,7 @@ jobs:
- job: Windows
pool:
vmImage: win1803
vmImage: "windows-2019"
timeoutInMinutes: 60 # how long to run the job before automatically cancelling
steps:
- checkout: self # self represents the repo where the initial Pipelines YAML file was found

Просмотреть файл

@ -51,7 +51,7 @@ steps:
- task: AzureCLI@1
displayName: "Pull down old report and add updates"
inputs:
azureSubscription: 'PELITTLE TEAM - CSE DWR (d36d0808-a967-4f73-9fdc-32ea232fc81d)'
azureSubscription: 'pj-little-sub'
scriptLocation: inlineScript
inlineScript: './scripts/update-report.sh'

Просмотреть файл

@ -0,0 +1,28 @@
# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops#job-templates-with-parameters
jobs:
- job: ${{ parameters.name }}
pool: ${{ parameters.pool }}
timeoutInMinutes: 15 # how long to run the job before automatically cancelling
steps:
- task: NodeTool@0
displayName: 'Use Node 10.x'
inputs:
versionSpec: 10.x
- bash: |
set -ex
# clean install
npm ci
npm run release-ci
OS=${{ parameters.os }}
ARTIFACT_NAME=${{ parameters.artifact }}
mkdir -p ${OS}
cp releases/${ARTIFACT_NAME} ${OS}/
displayName: Build
- publish: $(System.DefaultWorkingDirectory)/${{ parameters.os }}
artifact: ${{ parameters.os }}

Просмотреть файл

@ -0,0 +1,32 @@
parameters:
GitHubConnection: '' # defaults for any parameters that aren't specified
repositoryName: ''
releaseNotesSource: input
addChangeLog: false
isPreRelease: true
isDraft: true
jobs:
- job: Create_Github_Release
timeoutInMinutes: 30 # timeout on job if deploy is not completed in 30 minutes
pool:
vmImage: ubuntu-16.04
steps:
- checkout: none # we already have the artifacts built, don't need the code
- download: current
- task: GitHubRelease@0
displayName: 'GitHub release (create)'
inputs:
gitHubConnection: ${{ parameters.GitHubConnection }}
repositoryName: ${{ parameters.repositoryName }}
releaseNotesSource: ${{ parameters.releaseNotesSource }}
target: $(Build.SourceBranch)
assets: |
../linux/*
../windows/*
../mac/*
addChangeLog: ${{ parameters.addChangeLog }}
isDraft: true # for testing, change to true when ready to merge
isPreRelease: ${{ parameters.isPrelease }}

51
package-lock.json сгенерированный
Просмотреть файл

@ -6253,6 +6253,14 @@
}
}
},
"express-request-id": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/express-request-id/-/express-request-id-1.4.1.tgz",
"integrity": "sha512-qpxK6XhDYtdx9FvxwCHkUeZVWtkGbWR87hBAzGECfwYF/QQCPXEwwB2/9NGkOR1tT7/aLs9mma3CT0vjSzuZVw==",
"requires": {
"uuid": "^3.3.2"
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@ -7319,8 +7327,7 @@
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"optional": true
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"aproba": {
"version": "1.2.0",
@ -7341,14 +7348,12 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"optional": true
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -7363,20 +7368,17 @@
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"optional": true
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"optional": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"optional": true
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"core-util-is": {
"version": "1.0.2",
@ -7493,8 +7495,7 @@
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"optional": true
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ini": {
"version": "1.3.5",
@ -7506,7 +7507,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -7521,7 +7521,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -7529,14 +7528,12 @@
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"optional": true
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"minipass": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz",
"integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==",
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@ -7555,7 +7552,6 @@
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -7636,8 +7632,7 @@
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"optional": true
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"object-assign": {
"version": "4.1.1",
@ -7649,7 +7644,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"optional": true,
"requires": {
"wrappy": "1"
}
@ -7735,8 +7729,7 @@
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
"optional": true
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
},
"safer-buffer": {
"version": "2.1.2",
@ -7772,7 +7765,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -7792,7 +7784,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -7836,14 +7827,12 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"optional": true
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"yallist": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz",
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=",
"optional": true
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k="
}
}
},
@ -14828,7 +14817,7 @@
"react-modal": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.8.1.tgz",
"integrity": "sha1-cwD5Sm+SouF5lN4L5sy2FzRGTJ4=",
"integrity": "sha512-aLKeZM9pgXpIKVwopRHMuvqKWiBajkqisDA8UzocdCF6S4fyKVfLWmZR5G1Q0ODBxxxxf2XIwiCP8G/11GJAuw==",
"requires": {
"exenv": "^1.2.0",
"prop-types": "^15.5.10",

Просмотреть файл

@ -23,6 +23,7 @@
"buffer-reverse": "^1.0.1",
"crypto-js": "^3.1.9-1",
"dotenv": "^7.0.0",
"express-request-id": "^1.4.1",
"google-protobuf": "^3.6.1",
"jpeg-js": "^0.3.4",
"json2csv": "^4.5.0",

Просмотреть файл

@ -0,0 +1,88 @@
trigger: none # manual queue only when we're ready to release
pr: none # disable CI build for PR
stages:
- stage: version_bump_commit
jobs:
- job: "version_bump"
variables:
- group: GitHub-Deploy-Creds
timeoutInMinutes: 30 # timeout on job if deploy is not completed in 30 minutes
cancelTimeoutInMinutes: 1 # time limit to wait for job to cancel
pool:
vmImage: macOS-10.15 # ssh key was generated on a Mac so using the same type of OS here
steps:
- task: NodeTool@0
inputs:
versionSpec: 10.x
displayName: 'Install Node.js'
# Download secure file
# Download a secure file to the agent machine
- task: DownloadSecureFile@1
# name: sshKey # The name with which to reference the secure file's path on the agent, like $(mySecureFile.secureFilePath)
inputs:
secureFile: vott_id_rsa
# Install an SSH key prior to a build or deployment
- task: InstallSSHKey@0 # https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/install-ssh-key?view=azure-devops
inputs:
knownHostsEntry: $(KNOWN_HOSTS_ENTRY)
sshPublicKey: $(SSH_PUBLIC_KEY)
#sshPassphrase: # Optional
sshKeySecureFile: vott_id_rsa
env:
KNOWN_HOSTS_ENTRY: $(KNOWN_HOSTS_ENTRY)
SSH_PUBLIC_KEY: $(SSH_PUBLIC_KEY) # map to the right format (camelCase) that Azure credentials understand
- task: Bash@3
name: BumpNpmVersion
displayName: Bump NPM Prerelease Version
inputs:
targetType: filePath
filePath: ./scripts/version-bump-commit.sh
env:
SOURCE_BRANCH: $(Build.SourceBranch)
- stage: package_build
dependsOn: version_bump_commit
jobs:
- template: azure-pipelines/templates/build-artifact.yml
parameters:
name: Linux
pool:
vmImage: ubuntu-16.04
os: linux
artifact: vott*.snap
- template: azure-pipelines/templates/build-artifact.yml
parameters:
name: Windows
pool:
vmImage: "windows-2019"
os: windows
artifact: vott*.exe
- template: azure-pipelines/templates/build-artifact.yml
parameters:
name: MacOS
pool:
vmImage: macOS-10.15
os: mac
artifact: vott*.dmg
- stage: github_release
dependsOn: package_build
jobs:
- template: azure-pipelines/templates/create-github-release.yml
parameters:
GitHubConnection: 'GitHub connection' # defaults for any parameters that aren't specified
repositoryName: 'Microsoft/VoTT'
releaseNotesSource: input
addChangeLog: false
isPreRelease: true
isDraft: false

Просмотреть файл

@ -16,7 +16,7 @@ jobs:
- job: MacOS
condition: succeeded()
pool:
vmImage: macOS-10.13
vmImage: macOS-10.15
timeoutInMinutes: 60 # how long to run the job before automatically cancelling
steps:
- checkout: self # self represents the repo where the initial Pipelines YAML file was found
@ -26,7 +26,7 @@ jobs:
- job: Windows
condition: succeeded()
pool:
vmImage: vs2017-win2016
vmImage: "windows-2019"
timeoutInMinutes: 60 # how long to run the job before automatically cancelling
steps:
- checkout: self # self represents the repo where the initial Pipelines YAML file was found

Просмотреть файл

@ -0,0 +1,28 @@
#!/bin/bash
set -euo pipefail
NPM_RELEASE_TYPE=${1-"prepatch --preid=preview"}
# Get full branch name excluding refs/head from the env var SOURCE_BRANCH
SOURCE_BRANCH_NAME=${SOURCE_BRANCH/refs\/heads\/}
# Configure git to commit as SLS Azure Functions Service Account
echo "Configuring git to use deploy key..."
git config --local user.email "vott@microsoft.com"
git config --local user.name "Vott"
echo "SOURCE_BRANCH: ${SOURCE_BRANCH_NAME}"
git pull origin ${SOURCE_BRANCH_NAME}
git checkout ${SOURCE_BRANCH_NAME}
echo "Checked out branch: ${SOURCE_BRANCH_NAME}"
NPM_VERSION=`npm version ${NPM_RELEASE_TYPE} -m "release: Update ${NPM_RELEASE_TYPE} version to %s ***NO_CI***"`
echo "Set NPM version to: ${NPM_VERSION}"
SHA=`git rev-parse HEAD`
export GIT_SSH_COMMAND="ssh -vvv -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
git remote add authOrigin git@github.com:microsoft/VoTT.git
git push authOrigin ${SOURCE_BRANCH_NAME} --tags
echo "Pushed new tag: ${NPM_VERSION} @ SHA: ${SHA:0:8}"

5
server/.env.template Normal file
Просмотреть файл

@ -0,0 +1,5 @@
APP_ID=xyz
APP_SECRET=asdf
COOKIE_SECRETS="[ { key: '12345678901234567890123456789012', iv: '123456789012' }, { key: 'abcdefghijklmnopqrstuvwxyzabcdef', iv: 'abcdefghijkl' }, ])"
ALLOW_HTTP=true
BASE_URL=http://localhost:3000/

35
server/.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,35 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch via NPM",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run-script",
"debug"
],
"port": 9229
},
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/lib/app.js", //"${workspaceFolder}\\lib\\app.js",
"args": [
"|",
"bunyan"
],
"outFiles": [
"${workspaceFolder}/**/*.js"
],
"console": "internalConsole",
"outputCapture": "std",
}
]
}

24
server/.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,24 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build",
"problemMatcher": [
"$tsc"
],
"group": "build"
},
{
"type": "typescript",
"tsconfig": "tsconfig.json",
"option": "watch",
"problemMatcher": [
"$tsc-watch"
],
"group": "build"
}
]
}

75
server/azure-deploy.yml Normal file
Просмотреть файл

@ -0,0 +1,75 @@
# Node.js
# Build a general Node.js project with npm.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
trigger: none
pr: none
variables:
# Azure Resource Manager connection created during pipeline creation
azureSubscription: fe7b93fe-e836-4a55-804c-883dbea6af24'
# Web app name
webAppName: 'vott'
# Agent VM image name
vmImageName: 'ubuntu-latest'
stages:
- stage: Build
displayName: Build stage
jobs:
- job: Build
displayName: Build
pool:
vmImage: $(vmImageName)
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js'
- script: |
npm install
npm run build --if-present
# npm run test --if-present
workingDirectory: $(System.DefaultWorkingDirectory)/server
displayName: 'npm install, build and test'
- task: ArchiveFiles@2
displayName: 'Archive files'
inputs:
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/server'
includeRootFolder: false
archiveType: zip
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
replaceExistingArchive: true
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
artifact: drop
- stage: Deploy
displayName: Deploy stage
dependsOn: Build
condition: succeeded()
jobs:
- deployment: Deploy
displayName: Deploy
environment: 'development'
pool:
vmImage: $(vmImageName)
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Azure Web App Deploy: vott'
inputs:
azureSubscription: $(azureSubscription)
appType: webAppLinux
appName: $(webAppName)
runtimeStack: 'NODE|10.10'
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
startUpCommand: 'npm run start'

6
server/jest.config.js Normal file
Просмотреть файл

@ -0,0 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
rootDir: "src",
coverageDirectory: "../coverage",
};

7320
server/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

62
server/package.json Normal file
Просмотреть файл

@ -0,0 +1,62 @@
{
"name": "vott-server",
"version": "1.0.0",
"description": "Server to support VoTT with login",
"main": "./lib/app.js",
"scripts": {
"build": "tsc",
"start": "node ./lib/app.js",
"test:unit": "jest --runInBand",
"test": "npm run lint && npm run test:unit",
"watch": "concurrently --kill-others \"tsc -w\" \"nodemon --inspect ./lib/app.js\"",
"lint": "tslint -q -p . -c tslint.json",
"lint:fix": "tslint --fix -p . -c tslint.json",
"debug": "nodemon --inspect ./lib/app.js | bunyan"
},
"author": "Microsoft",
"license": "MIT",
"dependencies": {
"@microsoft/microsoft-graph-client": "^1.7.0",
"body-parser": "^1.15.2",
"bunyan": "*",
"cookie-parser": "^1.4.3",
"cookie-session": "^1.3.3",
"cookies": "^0.7.3",
"ejs": ">= 0.0.0",
"ejs-locals": ">= 0.0.0",
"express": "^4.17.1",
"express-request-id": "^1.4.1",
"method-override": "^3.0.0",
"morgan": "^1.9.1",
"node-fetch": "^2.6.0",
"passport": "*",
"passport-azure-ad": "^4.1.0",
"simple-oauth2": "^2.2.1"
},
"devDependencies": {
"@types/bunyan": "^1.8.6",
"@types/cookie-parser": "^1.4.2",
"@types/cookie-session": "^2.0.37",
"@types/cookies": "^0.7.2",
"@types/dotenv": "^6.1.1",
"@types/express": "^4.17.1",
"@types/express-request-id": "^1.4.1",
"@types/jest": "^24.0.17",
"@types/method-override": "0.0.31",
"@types/morgan": "^1.7.37",
"@types/node-fetch": "^2.5.0",
"@types/passport": "^1.0.1",
"@types/passport-azure-ad": "^4.0.3",
"@types/simple-oauth2": "^2.2.1",
"concurrently": "^4.1.1",
"dotenv": "^8.1.0",
"jest": "^24.8.0",
"nodemon": "^1.19.1",
"ts-jest": "^24.0.2",
"tslint": "^5.18.0",
"typescript": "^3.6.2"
},
"engines": {
"node": ">= 10.0.0"
}
}

71
server/public/test.html Normal file
Просмотреть файл

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html>
<head>
<title>Simple Integration Tests</title>
</head>
<body>
<h2>Simple integration tests</h2>
<p>You must be logged in to use tests</p>
<button onclick="test_get_me()">GET /api/v1.0/me</button><br />
<button onclick="test_get_profile()">GET /api/v1.0/profile</button><br />
<button onclick="test_put_profile()">PUT /api/v1.0/profile { "foo": "bar" }</button><br />
<br />
<button onclick="test_get_connection()">GET /api/v1.0/cloudconnections/connection1</button><br />
<button onclick="test_put_connection()">PUT /api/v1.0/cloudconnections/connection1 { "foo": "bar" }</button><br />
<button onclick="test_put_connection_alt()">PUT /api/v1.0/cloudconnections/connection1 { "foo": "baz" }</button><br />
<button onclick="test_patch_connection()">PATCH /api/v1.0/cloudconnections/connection1 { "updated": ${now} }</button><br />
<button onclick="test_delete_connection()">DELETE /api/v1.0/cloudconnections/connection1</button><br />
<pre id='result'></pre>
<script>
function fetchOptions(method, thing) {
return { method, body: JSON.stringify(thing), headers: { 'Content-Type': 'application/json' } };
}
async function displayResponse(response) {
let json = null;
try { json = await response.json().catch(); } catch (err) { }
let result = (response.ok ? '*success*' : '*failed*') + '\n' + (json ? JSON.stringify(json, undefined, 2) : '');
document.getElementById("result").innerHTML = result;
}
async function test_get_me(e) {
let response = await fetch('/api/v1.0/me');
displayResponse(response);
}
async function test_get_profile(e) {
let response = await fetch('/api/v1.0/profile');
displayResponse(response);
}
async function test_put_profile(e) {
let response = await fetch('/api/v1.0/profile', fetchOptions("PUT", { foo: "bar" }));
displayResponse(response);
}
async function test_get_connection(e) {
let response = await fetch('/api/v1.0/cloudconnections/connection1');
displayResponse(response);
}
async function test_put_connection(e) {
let response = await fetch('/api/v1.0/cloudconnections/connection1', fetchOptions("PUT", { foo: "bar" }));
displayResponse(response);
}
async function test_put_connection_alt(e) {
let response = await fetch('/api/v1.0/cloudconnections/connection1', fetchOptions("PUT", { foo: "baz" }));
displayResponse(response);
}
async function test_patch_connection(e) {
let now = (new Date(Date.now())).toISOString();
let response = await fetch('/api/v1.0/cloudconnections/connection1', fetchOptions("PATCH", { updated: now }));
displayResponse(response);
}
async function test_delete_connection(e) {
let response = await fetch('/api/v1.0/cloudconnections/connection1', fetchOptions("DELETE", undefined));
displayResponse(response);
}
</script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,8 @@
<% if (!user) { %>
<h2>Welcome! Please log in.</h2>
<a href="/login">Log In</a>
<% } else { %>
<p>Profile ID: <%= user.oid %></p>
<p>Email: <%= user.mail %></p>
<pre><%- JSON.stringify(user, null, 2) %></pre>
<% } %>

Просмотреть файл

@ -0,0 +1,14 @@
<% if (!user) { %>
<h2>Welcome! Please log in.</h2>
<a href="/login">Log In</a></br>
<a href="https://myapps.microsoft.com">Manage your permissions (organization)</a></br>
<a href="https://account.live.com/consent/Manage">Manage account permissions (personal)</a>
<% } else { %>
<h2>Hello, <%= user.displayName %></h2>
<a href="/account">Account Info</a></br>
<a href="/public/test.html">Run tests</a></br>
<a href="/endsession">End session</a></br>
<a href="/logout">Log Out</a></br>
<a href="https://myapps.microsoft.com">Manage your permissions (organization)</a></br>
<a href="https://account.live.com/consent/Manage">Manage account permissions (personal)</a>
<% } %>

Просмотреть файл

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>Passport-OpenID Example</title>
</head>
<body>
<% if (!user) { %>
<p>
<a href="/">Home</a> |
<a href="/login">Log In</a>
</p>
<% } else { %>
<p>
<a href="/">Home</a> |
<a href="/account">Account</a> |
<a href="/logout">Log Out</a>
</p>
<% } %>
<%- body %>
</body>
</html>

Просмотреть файл

@ -0,0 +1,50 @@
// import { default as fetch } from 'node-fetch';
import * as config from '../config';
import { app, server } from '../app';
import { ServerResponse } from 'http';
beforeAll(async (done) => {
if (!server.listening) {
server.on('listening', done())
} else {
done();
}
});
afterAll(async (done) => {
server.close(() => {
console.log('done');
done();
});
});
describe('App Server', () => {
afterAll(async (done) => {
server.close(() => {
console.log('done');
done();
});
});
test('initialized', async (done) => {
expect(app.name).toBeDefined();
expect(server.listening).toBe(true);
done();
});
test('loads app.html', async (done) => {
const response = await fetch(config.baseUrl);
expect(response.status).toBe(200);
done();
});
test('redirects to login', async (done) => {
const response = await fetch(config.baseUrl + '/api/v1.0/me');
expect(response.status).toBe(404);
done();
});
});

Просмотреть файл

@ -0,0 +1,13 @@
// import { default as fetch } from 'node-fetch';
import * as config from '../config';
describe('App Server', () => {
test('should be closed', async (done) => {
// do nothing
done();
});
});

364
server/src/app.ts Normal file
Просмотреть файл

@ -0,0 +1,364 @@
import * as bodyParser from 'body-parser';
import * as morgan from 'morgan';
import * as bunyan from 'bunyan';
import * as cookieParser from 'cookie-parser';
import * as express from 'express';
import cookieSession = require('cookie-session');
import * as methodOverride from 'method-override';
import * as passport from 'passport';
import * as passportAzureAD from 'passport-azure-ad';
import * as config from './config';
import * as path from 'path';
import * as express_request_id from 'express-request-id';
import * as simple_oath2 from 'simple-oauth2';
import * as graph from './graph';
import * as oauth2 from './oauth2';
export const log = bunyan.createLogger({
name: 'BUNYAN-LOGGER',
src: true,
});
// -----------------------------------------------------------------------------
// Passport Setup Follows
// -----------------------------------------------------------------------------
// Setup schemas for Passport's User object and the the session persistence format.
// Augument Passport's request.user with the Azure AD oauthToken
declare global {
namespace Express {
interface User {
oid: string;
oauthToken: oauth2.Token;
}
}
}
type FullUserSchema = Express.User; // This now includes the above declaration for User.
interface MinimizedUserSchema {
oid: string;
refresh_token: string;
}
passport.serializeUser((user: FullUserSchema, done) => {
const stored: MinimizedUserSchema = { oid: user.oid, refresh_token: user.oauthToken.refresh_token };
done(null, stored);
});
passport.deserializeUser(async (stored: MinimizedUserSchema, done) => {
if (!stored || !stored.refresh_token) { return done(Error('no user profile')); }
let oauthClient = await oauth2.client(stored);
if (oauthClient.expired()) {
oauthClient = await oauthClient.refresh(); // must reassign here.
}
const profile = await graph.user(oauthClient.token.access_token).catch(reason => { log.error('could not retrieve profile', reason); });
if (!profile) { return done(Error('no user profile')); }
const result = { ...profile, oid: stored.oid, oauthToken: oauthClient.token };
return done(null, result);
});
// -----------------------------------------------------------------------------
// Define the AzureAD OIDCStrategy Strategy
// -----------------------------------------------------------------------------
const OIDCStrategyTemplate = {} as passportAzureAD.IOIDCStrategyOptionWithoutRequest;
const azureStrategyOptions: passportAzureAD.IOIDCStrategyOptionWithRequest = {
identityMetadata: config.creds.identityMetadata,
clientID: config.creds.clientID,
responseType: config.creds.responseType as typeof OIDCStrategyTemplate.responseType,
responseMode: config.creds.responseMode as typeof OIDCStrategyTemplate.responseMode,
redirectUrl: config.creds.redirectUrl,
allowHttpForRedirectUrl: config.creds.allowHttpForRedirectUrl,
clientSecret: config.creds.clientSecret,
validateIssuer: config.creds.validateIssuer,
isB2C: config.creds.isB2C,
issuer: config.creds.issuer,
passReqToCallback: true,
scope: config.creds.scope,
loggingLevel: config.creds.logLevel as typeof OIDCStrategyTemplate.loggingLevel,
nonceLifetime: config.creds.nonceLifetime,
nonceMaxAmount: config.creds.nonceMaxAmount,
useCookieInsteadOfSession: config.creds.useCookieInsteadOfSession,
cookieEncryptionKeys: config.creds.cookieEncryptionKeys,
clockSkew: config.creds.clockSkew,
};
// -----------------------------------------------------------------------------
// Use the Azure OIDCStrategy within Passport.
//
// Strategies in passport require a `verify` function, which accepts credentials
// (in this case, the `oid` claim in id_token), and invoke a callback to find
// the corresponding user object.
//
// The following are the accepted prototypes for the `verify` function
// (1) function(iss, sub, done)
// (2) function(iss, sub, profile, done)
// (3) function(iss, sub, profile, access_token, refresh_token, done)
// (4) function(iss, sub, profile, access_token, refresh_token, params, done)
// (5) function(iss, sub, profile, jwtClaims, access_token, refresh_token, params, done)
// (6) prototype (1)-(5) with an additional `req` parameter as the first parameter
//
// To do prototype (6), passReqToCallback must be set to true in the config.
// -----------------------------------------------------------------------------
passport.use(new passportAzureAD.OIDCStrategy(azureStrategyOptions, processAzureStrategy));
async function processAzureStrategy(req: express.Request,
iss: string, sub: string, profile: passportAzureAD.IProfile, jwtClaims: any,
access_token: string, refresh_token: string, oauthToken: any,
done: passportAzureAD.VerifyCallback) {
if (!profile.oid) {
return done(new Error('No oid found'), null);
}
// asynchronous verification, for effect...
process.nextTick(async () => {
const fullProfile = await graph.user(access_token);
if (!fullProfile) {
return done(Error('no profile'));
}
const oauth = oauth2.client(oauthToken);
const result = { ...fullProfile, oid: profile.oid, oauthToken: oauth.token };
return done(null, result);
});
}
// -----------------------------------------------------------------------------
// Config the express app and all the required middleware
// -----------------------------------------------------------------------------
export const app = express();
app.use(morgan(config.httpLogFormat));
app.set('trust proxy', true);
app.set('views', path.join(__dirname, '../public/views'));
app.set('view engine', 'ejs');
app.use(express_request_id());
app.use(methodOverride());
app.use(cookieParser());
app.use(cookieSession({ keys: config.creds.cookieEncryptionKeys.map(value => value.key), secure: false, maxAge: 1000 * 60 * 60 * 24 * 365 }));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(passport.initialize());
app.use(passport.session());
// -----------------------------------------------------------------------------
// Define a couple of authencation request handlers.
// -----------------------------------------------------------------------------
function ensureAuthenticated(req: express.Request, res: express.Response, next: express.NextFunction) {
if (req.isAuthenticated()) { return next(); }
res.redirect('/login');
}
function ensureAuthenticatedApi(req: express.Request, res: express.Response, next: express.NextFunction) {
if (req.isAuthenticated()) { return next(); }
res.sendStatus(401).end();
return next();
}
app.get('/', (req, res) => {
const user = { ...req.user, oauthToken: '[removed]'};
res.render('index', { user });
});
app.get('/account', ensureAuthenticated, (req, res, next) => {
const user = { ...req.user, oauthToken: '[removed]'};
res.render('account', { user });
});
app.get('/login', (req, res, next) => {
passport.authenticate('azuread-openidconnect',
{
response: res, // required
customState: 'my_state', // optional. Provide a value if you want to provide custom state value.
failureRedirect: '/',
} as passport.AuthenticateOptions,
)(req, res, next);
},
(req, res) => {
log.info('login was called');
res.redirect('/');
});
// 'GET returnURL'
// `passport.authenticate` will try to authenticate the content returned in
// query (such as authorization code). If authentication fails, user will be
// redirected to '/' (home page); otherwise, it passes to the next middleware.
app.get('/auth/openid/return', (req, res, next) => {
passport.authenticate('azuread-openidconnect',
{
response: res, // required
failureRedirect: '/',
} as passport.AuthenticateOptions,
)(req, res, next);
},
(req, res, next) => {
log.info('received a return from AzureAD.');
res.redirect('/');
});
// 'POST returnURL'
// `passport.authenticate` will try to authenticate the content returned in
// body (such as authorization code). If authentication fails, user will be
// redirected to '/' (home page); otherwise, it passes to the next middleware.
app.post('/auth/openid/return',
(req, res, next) => {
passport.authenticate('azuread-openidconnect',
{
response: res, // required
failureRedirect: '/',
} as passport.AuthenticateOptions,
)(req, res, next);
},
(req, res, next) => {
log.info('received a return from AzureAD.');
res.redirect('/');
});
// 'endsession' route, logout from passport, and destroy the session with AAD.
app.get('/endsession', (req, res) => {
req.session = null;
req.logOut();
res.redirect('/');
});
// 'logout' route, logout from passport, and destroy the session with AAD.
app.get('/logout', (req, res) => {
req.session = null;
// req.session.destroy((err) => {
req.logOut();
res.redirect(config.destroySessionUrl);
});
app.get('/api/v1.0/me', ensureAuthenticatedApi,
async (req, res, next) => {
try {
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const result = await graph.user(oauth.token.access_token);
res.json(result);
res.end();
next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.get('/api/v1.0/profile', ensureAuthenticatedApi,
async (req, res, next) => {
try {
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const result = await graph.client(oauth.token.access_token).api('/me/extensions/com.code-with.vott').get();
res.json(result);
res.end();
next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.put('/api/v1.0/profile', ensureAuthenticatedApi,
async (req, res, next) => {
try {
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const body = { ...req.body, extensionName: 'com.code-with.vott' };
let result = null;
try { // Handle bad graph open extension semantics
result = await graph.client(oauth.token.access_token).api('/me/extensions/').post(body);
} catch (error) {
// if it already exists and we are replacing. Delete and try again.
result = await graph.client(oauth.token.access_token).api('/me/extensions/com.code-with.vott').delete();
result = await graph.client(oauth.token.access_token).api('/me/extensions').post(body);
}
res.json(result);
res.end();
next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.get('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
async (req, res, next) => {
try {
const id = req.params.id; // careful should only be a domain name pattern.
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const result = await graph.client(oauth.token.access_token).api(`/me/extensions/com.code-with.vott.${id}`).get();
res.json(result);
res.end();
next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.put('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
async (req, res, next) => {
try {
const id = req.params.id; // careful should only be a domain name pattern.
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const body = { ...req.body, extensionName: `com.code-with.vott.${id}` };
let result = null;
try { // Handle bad graph open extension semantics
result = await graph.client(oauth.token.access_token).api('/me/extensions/').post(body);
} catch (error) {
// if it already exists and we are replacing. Delete and try again.
result = await graph.client(oauth.token.access_token).api(`/me/extensions/com.code-with.vott.${id}`).delete();
result = await graph.client(oauth.token.access_token).api('/me/extensions').post(body);
}
res.json(result);
res.end();
next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.patch('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
async (req, res, next) => {
try {
const id = req.params.id; // careful should only be a domain name pattern.
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const body = { ...req.body, extensionName: `com.code-with.vott.${id}` };
const result = await graph.client(oauth.token.access_token).api(`/me/extensions/com.code-with.vott.${id}`).patch(body);
res.json(result);
res.end();
next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.delete('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
async (req, res, next) => {
try {
const id = req.params.id; // careful should only be a domain name pattern.
let oauth = oauth2.client(req.user.oauthToken);
if (oauth.expired()) { oauth = await oauth.refresh(); }
const result = await graph.client(oauth.token.access_token).api(`/me/extensions/com.code-with.vott.${id}`).delete();
res.end();
return next();
} catch (error) {
res.status(error.statusCode).json(error).end();
return next();
}
});
app.use('/public', express.static(path.join(__dirname, '../public')));
export const server = app.listen(config.port);

94
server/src/config.ts Normal file
Просмотреть файл

@ -0,0 +1,94 @@
// tslint:disable-next-line: no-var-requires
require('dotenv').config();
export const baseUrl = process.env.BASE_URL || 'http://localhost:3000/';
export const redirectPath = 'auth/openid/return';
export const port = process.env.PORT || '3000';
export const loggingLevel = process.env.LOGGING_LEVEL || 'info';
export const httpLogFormat = process.env.HTTP_LOG_FORMAT || 'dev';
console.log('config values', process.env.APP_ID, process.env.APP_SECRET, baseUrl, redirectPath, port, loggingLevel, httpLogFormat);
export const creds = {
// Required
identityMetadata: 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration',
// 'https://login.microsoftonline.com/<tenant_name>.onmicrosoft.com/v2.0/.well-known/openid-configuration',
// or equivalently: 'https://login.microsoftonline.com/<tenant_guid>/v2.0/.well-known/openid-configuration'
//
// or you can use the common endpoint
// 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration'
// To use the common endpoint, you have to either turn `validateIssuer` off, or provide the `issuer` value.
// Required, the client ID of your app in AAD
clientID: process.env.APP_ID,
// Required, must be 'code', 'code id_token', 'id_token code' or 'id_token'
// If you want to get access_token, you must use 'code', 'code id_token' or 'id_token code'
responseType: 'code id_token',
// Required
responseMode: 'form_post',
// Required, the reply URL registered in AAD for your app
redirectUrl: baseUrl + redirectPath,
// Required if we use http for redirectUrl
allowHttpForRedirectUrl: process.env.ALLOW_HTTP ? process.env.ALLOW_HTTP === 'true' : true,
// Required if `responseType` is 'code', 'id_token code' or 'code id_token'.
// If app key contains '\', replace it with '\\'.
clientSecret: process.env.APP_SECRET,
// Required to set to false if you don't want to validate issuer
validateIssuer: false,
// Required if you want to provide the issuer(s) you want to validate instead of using the issuer from metadata
// issuer could be a string or an array of strings of the following form: 'https://sts.windows.net/<tenant_guid>/v2.0'
issuer: null as string,
// !Bug - must be false in this sample
// Required to set to true if the `verify` function has 'req' as the first parameter
passReqToCallback: true,
// Recommended to set to true. By default we save state in express session, if this option is set to true, then
// we encrypt state and save it in cookie instead. This option together with { session: false } allows your app
// to be completely express session free.
useCookieInsteadOfSession: true,
logLevel: loggingLevel,
// Required if `useCookieInsteadOfSession` is set to true. You can provide multiple set of key/iv pairs for key
// rollover purpose. We always use the first set of key/iv pair to encrypt cookie, but we will try every set of
// key/iv pair to decrypt cookie. Key can be any string of length 32, and iv can be any string of length 12.
cookieEncryptionKeys: (process.env.COOKIES_SECRETS ? JSON.parse(process.env.COOKIES_SECRETS) :
[
{ key: '12345678901234567890123456789012', iv: '123456789012' },
{ key: 'abcdefghijklmnopqrstuvwxyzabcdef', iv: 'abcdefghijkl' },
]) as Array<{ key: string; iv: string; }>,
// The additional scopes we want besides 'openid'.
// 'profile' scope is required, the rest scopes are optional.
// (1) if you want to receive refresh_token, use 'offline_access' scope
// (2) if you want to get access_token for graph api, use the graph api url like 'https://graph.microsoft.com/mail.read'
scope: ['profile', /* 'offline_access', */ 'https://graph.microsoft.com/user.readwrite'],
// Optional. The lifetime of nonce in session or cookie, the default value is 3600 (seconds).
nonceLifetime: null as number,
// Optional. The max amount of nonce saved in session or cookie, the default value is 10.
nonceMaxAmount: 5,
// Optional. The clock skew allowed in token validation, the default value is 300 seconds.
clockSkew: null as number,
// Optional. Is B2C
isB2C: false,
};
// The url you need to go to destroy the session with AAD
export let destroySessionUrl = 'https://login.microsoftonline.com/common/oauth2/logout?post_logout_redirect_uri=http://localhost:3000';
if (!creds.clientID || !creds.clientSecret) {
console.log('issue with config');
throw Error('Missing configuration. You need a .env file or environment variables for APP_ID and APP_SECRET');
}

30
server/src/graph.ts Normal file
Просмотреть файл

@ -0,0 +1,30 @@
import * as graphClient from '@microsoft/microsoft-graph-client';
export async function user(access_token: string) {
const token = client(access_token);
const result = await token.api('/me').get();
return result;
}
export async function getEvents(access_token: string) {
const token = client(access_token);
const events = await token.api('/me/events')
.select('subject,organizer,start,end')
.orderby('createdDateTime DESC')
.get();
return events;
}
export function client(access_token: string): graphClient.Client {
// Initialize Graph client
const result = graphClient.Client.init({
// Use the provided access token to authenticate
// requests
authProvider: (done: (err: any, access_token: string) => void) => {
done(null, access_token);
},
});
return result;
}

27
server/src/oauth2.ts Normal file
Просмотреть файл

@ -0,0 +1,27 @@
import * as express from 'express';
import * as simple_oauth2 from 'simple-oauth2';
import * as config from './config';
export const oauth2 = simple_oauth2.create({
client: {
id: config.creds.clientID,
secret: config.creds.clientSecret,
},
auth: {
tokenHost: 'https://login.microsoftonline.com/common',
authorizePath: '/oauth2/v2.0/authorize',
tokenPath: '/oauth2/v2.0/token',
},
});
export interface Token {
refresh_token: string;
access_token?: string;
expires_at?: string | Date;
}
export function client(token: Token) {
token.expires_at = token.expires_at || new Date(0);
const result = oauth2.accessToken.create(token);
return result;
}

62
server/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,62 @@
{
"compilerOptions": {
/* Basic Options */
"target": "ES2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
"allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./lib", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
// "strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
// "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"exclude": [
"node_modules",
"coverage",
"data_models",
"public",
"lib",
"temp",
"jest.config.js",
"src/__tests__"
]
}

48
server/tslint.json Normal file
Просмотреть файл

@ -0,0 +1,48 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"linterOptions": {
"exclude": [
"lib",
"public",
"src/routes",
"jest.config.js"
]
},
"jsRules": {},
"rules": {
"no-console": false,
"arrow-parens": false,
"max-classes-per-file": false,
"ordered-imports": false,
"object-literal-sort-keys": false,
"align": false,
"interface-name": false,
"quotemark": [
true,
"single",
"avoid-escape",
"avoid-template"
],
"max-line-length": {
"severity": "warning",
"options": [
160,
{
"ignore-pattern": "^import |^export {(.*?)} | //"
}
]
},
"variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-leading-underscore",
"allow-pascal-case",
"allow-snake-case"
]
}
}
}

Просмотреть файл

@ -12,6 +12,16 @@ import { ErrorHandler } from "./react/components/common/errorHandler/errorHandle
describe("App Component", () => {
const defaultState: IApplicationState = initialState;
const store = createReduxStore(defaultState);
const electronMock = {
ipcRenderer: {
send: jest.fn(),
on: jest.fn(),
},
};
beforeAll(() => {
delete (window as any).require;
});
function createComponent() {
return mount(

Просмотреть файл

@ -136,7 +136,7 @@ export default class HtmlFileReader {
canvas.width = video.videoWidth;
const ctx = canvas.getContext("2d");
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.toBlob(resolve);
canvas.toBlob(resolve, "image/jpeg", 1.0);
};
video.onerror = reject;
if (refresh) {

Просмотреть файл

@ -0,0 +1,511 @@
import { IAppStrings } from "../strings";
/**
* App Strings for Japanese language from Google Translate
*/
export const japanese: IAppStrings = {
appName: "ビジュアル オブジェクトタグ付けツール", // Visual Object Tagging Tool,
common: {
displayName: "表示名", // Display Name,
description: "説明", // Description,
submit: "送信", // Submit,
cancel: "キャンセル", // Cancel,
save: "保存", // Save,
delete: "削除", // Delete,
provider: "プロバイダー", // Provider,
homePage: "ホーム ページ", // Home Page"
},
titleBar: {
help: "ヘルプ", // Help,
minimize: "最小化", // Minimize,
maximize: "最大化", // Maximize,
restore: "戻す", // Restore,
close: "閉じる", // Close"
},
homePage: {
newProject: "新規プロジェクト", // New Project,
openLocalProject: {
title: "ローカルプロジェクトを開く", // Open Local Project"
},
openCloudProject: {
title: "クラウドプロジェクトを開く", // Open Cloud Project,
selectConnection: "接続を選択", // Select a Connection"
},
recentProjects: "最近のプロジェクト", // Recent Projects,
deleteProject: {
title: "プロジェクトを削除", // Delete Project,
confirmation: "プロジェクトを削除してもいいですか", // Are you sure you want to delete project"
},
importProject: {
title: "プロジェクトをインポート", // Import Project,
confirmation: "プロジェクト ${project.file.name} プロジェクト設定を v2 形式に変換してもいいですか",
// Are you sure you want to conver project ${project.file.name} project settings to v2 format?
// We recommend you backup the project file first."
},
messages: {
deleteSuccess: "${project.name} を削除しました", // Successfully deleted ${project.name}"
},
},
appSettings: {
title: "アプリケーション設定", // Application Settings,
storageTitle: "ストレージ設定", // Storage Settings,
uiHelp: "設定の保存場所", // Where your settings are stored,
save: "設定の保存", // Save Settings,
securityToken: {
name: {
title: "名前", // Name"
},
key: {
title: "キー", // Key"
},
},
securityTokens: {
title: "セキュリティ トークン", // Security Tokens,
description: "セキュリティ トークンは、プロジェクト構成内の機密データを暗号化するために使用されます",
// Security tokens are used to encrypt sensitive data within your project configuration"
},
version: {
description: "バージョン:", // Version"
},
commit: "SHA をコミット", // Commit SHA,
devTools: {
description: "問題の診断に役立つアプリケーション開発者ツールを開く", // Open application developer tools to help diagnose issues,
button: "開発者ツールを開く", // Toggle Developer Tools"
},
reload: {
description: "現在の変更をすべて破棄して、アプリをリロード", // Reload the app discarding all current changes,
button: "アプリケーションをリフレッシュ", // Refresh Application"
},
messages: {
saveSuccess: "アプリケーション設定を正常に保存しました", // Successfully saved application settings"
},
},
projectSettings: {
title: "プロジェクト設定", // Project Settings,
securityToken: {
title: "セキュリティ トークン", // Security Token,
description: "プロジェクト ファイル内の機密データを暗号化するために使用されます", // Used to encrypt sensitive data within project file"
},
save: "プロジェクトを保存", // Save Project,
sourceConnection: {
title: "ソース接続", // Source Connection,
description: "アセットのロード元", // Where to load assets fro"
},
targetConnection: {
title: "ターゲット接続", // Target Connection,
description: "プロジェクトとエクスポートされたデータの保存場所", // Where to save the project and exported dat"
},
videoSettings: {
title: "ビデオ設定", // Video Settings,
description: "タグ付けにおけるフレームの抽出割合", // The rate at which frames are extracted for tagging.,
frameExtractionRate: "フレーム抽出率(ビデオ 1 秒あたりのフレーム数)", // Frame Extraction Rate (frames per a video second)"
},
addConnection: "接続を追加", // Add Connection,
messages: {
saveSuccess: "${project.name} プロジェクト設定を正常に保存しました", // Successfully saved ${project.name} project settings"
},
},
projectMetrics: {
title: "プロジェクト メトリック", // Project Metrics,
assetsSectionTitle: "アセット", // Assets,
totalAssetCount: "すべてのアセット", // Total Assets,
visitedAssets: "訪問済みアセット(${count}", // Visited Assets (${count}),
taggedAssets: "タグ付きアセット(${count}", // Tagged Assets (${count}),
nonTaggedAssets: "タグ付けされていないアセット(${count}", // Not Tagged Assets (${count}),
nonVisitedAssets: "未訪問ないアセット(${count}", // Not Visited Assets (${count}),
tagsSectionTitle: "タグ", // Tags & Labels,
totalRegionCount: "タグ付けされたすべての領域", // Total Tagged Regions,
totalTagCount: "すべてのタグ", // Total Tags,
avgTagCountPerAsset: "アセットごとの平均タグ", // Average tags per asset"
},
tags: {
title: "タグ", // Tags,
placeholder: "新しいタグを追加", // Add new tag,
editor: "タグ エディター", // Tags Editor,
modal: {
name: "タグ名", // Tag Name,
color: "タグの色", // Tag Color"
},
colors: {
white: "白", // White,
gray: "グレー", // Gray,
red: "赤", // Red,
maroon: "マルーン", // Maroon,
yellow: "黄", // Yellow,
olive: "オリーブ", // Olive,
lime: "ライム", // Lime,
green: "緑", // Green,
aqua: "アクア", // Aqua,
teal: "ティール", // Teal,
blue: "青", // Blue,
navy: "濃紺", // Navy,
fuschia: "赤紫", // Fuschia,
purple: "紫", // Purple"
},
warnings: {
existingName: "タグ名が既に存在します。別の名前を選んでください", // Tag name already exists. Choose another name,
emptyName: "空のタグ名を持つことはできません", // Cannot have an empty tag name,
unknownTagName: "不明", // Unknown"
},
toolbar: {
add: "新しいタグを追加", // Add new tag,
search: "タグを検索", // Search tags,
edit: "タグを編集", // Edit tag,
lock: "タグをロック", // Lock tag,
moveUp: "タグを上に移動", // Move tag up,
moveDown: "タグを下に移動", // Move tag down,
delete: "タグを削除", // Delete tag"
},
},
connections: {
title: "接続", // Connections,
details: "接続の詳細", // Connection Details,
settings: "接続設定", // Connection Settings,
instructions: "編集する接続を選択してください", // Please select a connection to edit,
save: "接続を保存", // Save Connection,
messages: {
saveSuccess: "${connection.name} を保存しました", // Successfully saved ${connection.name},
deleteSuccess: "${connection.name} を削除しました", // Successfully deleted ${connection.name}"
},
imageCorsWarning: "警告Web ブラウザーで VoTT を使用する場合、CORSクロス オリジン リソース共有)の制限により、" +
"Bing 画像検索の一部のアセットが正しくエクスポートされない場合があります。",
// Warning: When using VoTT in a Web browser, some assets from Bing Image Search may no export
// correctly due to CORS (Cross Origin Resource Sharing) restrictions.",
blobCorsWarning: "警告ソースまたはターゲット接続として使用するには、Azure Blob Storage アカウントで CORSクロス オリジン リソース共有)を有効にする必要があります。 ",
// Warning: CORS (Cross Domain Resource Sharing) must be enabled on the Azure Blob Storage account, in order
// to use i as a source or target connection. More information on enabling CORS can be found in the {0}",
azDocLinkText: "Azure ドキュメント", // Azure Documentation.,
providers: {
azureBlob: {
title: "Azure Blob Storage", // Azure Blob Storage,
description: "",
accountName: {
title: "アカウント名", // Account Name,
description: "",
},
containerName: {
title: "コンテナー名", // Container Name,
description: "",
},
sas: {
title: "SAS", // SAS,
description: "Blob Storage アカウントの認証に使用される共有アクセス署名",
// Shared access signature used to authenticate to the blob storage account"
},
createContainer: {
title: "コンテナーを作成", // Create Container,
description: "Blob Storage コンテナーがまだ存在しない場合は作成します",
// Creates the blob container if it does not already exist"
},
},
bing: {
title: "Bing 画像検索", // Bing Image Search,
options: "Bing 画像検索のオプション", // Bing Image Search Options,
apiKey: "APIキー", // API Key,
query: "クエリ", // Query,
aspectRatio: {
title: "アスペクト比", // Aspect Ratio,
all: "すべて", // All,
square: "正方形", // Square,
wide: "横長", // Wide,
tall: "縦長", // Tall"
},
},
local: {
title: "ローカル ファイル システム", // Local File System,
folderPath: "フォルダー パス", // Folder Path,
selectFolder: "フォルダーを選択", // Select Folder,
chooseFolder: "フォルダーを選択", // Choose Folder"
},
},
},
editorPage: {
width: "幅", // Width,
height: "高さ", // Height,
tagged: "タグ付き", // Tagged,
visited: "訪問済み", // Visited,
toolbar: {
select: "選択V", // Select (V),
pan: "パン", // Pan,
drawRectangle: "長方形を描く", // Draw Rectangle,
drawPolygon: "ポリゴンを描く", // Draw Polygon,
copyRectangle: "長方形をコピー", // Copy Rectangle,
copy: "領域をコピー", // Copy Regions,
cut: "領域をカット", // Cut Regions,
paste: "領域を貼り付け", // Paste Regions,
removeAllRegions: "すべてのリージョンを削除", // Remove All Regions,
previousAsset: "前のアセット", // Previous Asset,
nextAsset: "次のアセット", // Next Asset,
saveProject: "プロジェクトを保存", // Save Project,
exportProject: "プロジェクトをエクスポート", // Export Project,
activeLearning: "アクティブ ラーニング", // Active Learning"
},
videoPlayer: {
previousTaggedFrame: {
tooltip: "前のタグ付きフレーム", // Previous Tagged Frame"
},
nextTaggedFrame: {
tooltip: "次のタグ付きフレーム", // Next Tagged Frame"
},
previousExpectedFrame: {
tooltip: "前のフレーム", // Previous Frame"
},
nextExpectedFrame: {
tooltip: "次のフレーム", // Next Frame"
},
},
help: {
title: "ヘルプ メニューの切り替え", // Toggle Help Menu,
escape: "ヘルプメニューを抜ける", // Escape Help Menu"
},
assetError: "アセットを読み込めません", // Unable to load asset,
tags: {
hotKey: {
apply: "ホット キーでタグを適用", // Apply Tag with Hot Key,
lock: "ホット キーでタグをロック", // Lock Tag with Hot Key"
},
rename: {
title: "タグの名前を変更", // Rename Tag,
confirmation: "このタグの名前を変更してもいいですか",
// Are you sure you want to rename this tag? It will be renamed throughout all assets"
},
delete: {
title: "タグを削除", // Delete Tag,
confirmation: "このタグを削除してもいいですか。このタグはすべてのアセットで削除され、このタグのみのあらゆる領域も削除されます",
// Are you sure you want to delete this tag? It will be deleted throughout all assets
// and any regions where this is the only tag will also be deleted"
},
},
canvas: {
removeAllRegions: {
title: "すべてのリージョンを削除", // Remove All Regions,
confirmation: "すべてのリージョンを削除してもいいですか", // Are you sure you want to remove all regions"
},
},
messages: {
enforceTaggedRegions: {
title: "無効な領域が検出されました", // Invalid region(s) detected,
description: "1 つ以上の領域にタグが付けられていません。次のアセットに進む前に、すべての領域をタグ付けしてください。",
// 1 or more regions have not been tagged. Ensure all regions ar tagged before continuing to next asset"
},
},
},
export: {
title: "エクスポート", // Export,
settings: "エクスポート設定", // Export Settings,
saveSettings: "エクスポート設定を保存", // Save Export Settings,
providers: {
common: {
properties: {
assetState: {
title: "アセットの状態", // Asset State,
description: "エクスポートに含めるアセット", // Which assets to include in the export,
options: {
all: "すべてのアセット", // All Assets,
visited: "訪問済みのアセットのみ", // Only Visited Assets,
tagged: "タグ付きアセットのみ", // Only tagged Assets"
},
},
testTrainSplit: {
title: "テスト/トレーニング分割", // Test / Train Split,
description: "エクスポートされたデータに使用するテスト/トレーニングの分割",
// The test train split to use for exported data"
},
includeImages: {
title: "画像を含める", // Include Images,
description: "ターゲット接続にバイナリ画像アセットを含めるかどうか",
// Whether or not to include binary image assets in target connection"
},
},
},
vottJson: {
displayName: "VoTT JSON", // VoTT JSO"
},
azureCV: {
displayName: "Azure Custom Vision サービス", // Azure Custom Vision Service,
regions: {
australiaEast: "オーストラリア東部", // Australia East,
centralIndia: "インド中部", // Central India,
eastUs: "米国東部", // East US,
eastUs2: "米国東部 2", // East US 2,
japanEast: "東日本", // Japan East,
northCentralUs: "米国中北部", // North Central US,
northEurope: "北ヨーロッパ", // North Europe,
southCentralUs: "アメリカ中南部", // South Central US,
southeastAsia: "東南アジア", // Southeast Asia,
ukSouth: "英国南部", // UK South,
westUs2: "米国西部 2", // West US 2,
westEurope: "西ヨーロッパ", // West Europe"
},
properties: {
apiKey: {
title: "API キー", // API Key"
},
region: {
title: "領域", // Region,
description: "サービスがデプロイされている Azure リージョン", // The Azure region where your service is deployed"
},
classificationType: {
title: "分類タイプ", // Classification Type,
options: {
multiLabel: "画像ごとに複数のタグ", // Multiple tags per image,
multiClass: "画像ごとに単一のタグ", // Single tag per image"
},
},
name: {
title: "プロジェクト名", // Project Name"
},
description: {
title: "プロジェクトの説明", // Project Description"
},
domainId: {
title: "ドメイン", // Domain"
},
newOrExisting: {
title: "新規または既存プロジェクト", // New or Existing Project,
options: {
new: "新規プロジェクト", // New Project,
existing: "既存プロジェクト", // Existing Project"
},
},
projectId: {
title: "プロジェクト名", // Project Name"
},
projectType: {
title: "プロジェクトの種類", // Project Type,
options: {
classification: "分類", // Classification,
objectDetection: "物体検出", // Object Detection"
},
},
},
},
tfRecords: {
displayName: "TensorFlow レコード", // Tensorflow Record"
},
pascalVoc: {
displayName: "Pascal VOC", // Pascal VOC,
exportUnassigned: {
title: "未割り当てをエクスポート", // Export Unassigned,
description: "エクスポートされたデータに未割り当てのタグを含めるかどうか",
// Whether or not to include unassigned tags in exported data"
},
},
cntk: {
displayName: "Microsoft Cognitive ToolkitCNTK", // Microsoft Cognitive Toolkit (CNTK)"
},
csv: {
displayName: "コンマ区切り値CSV", // Comma Separated Values (CSV)"
},
},
messages: {
saveSuccess: "エクスポート設定を保存しました", // Successfully saved export settings"
},
},
activeLearning: {
title: "アクティブ ラーニング", // Active Learning,
form: {
properties: {
modelPathType: {
title: "モデル プロバイダー", // Model Provider,
description: "トレーニング モデルのロード元", // Where to load the training model from,
options: {
preTrained: "事前トレーニング済みの Coco SSD", // Pre-trained Coco SSD,
customFilePath: "カスタム(ファイル パス)", // Custom (File path),
customWebUrl: "カスタムURL", // Custom (Url)"
},
},
autoDetect: {
title: "自動検出", // Auto Detect,
description: "アセット間を移動するときに自動的に予測を行うかどうか",
// Whether or not to automatically make predictions as you navigate between assets"
},
modelPath: {
title: "モデル パス", // Model path,
description: "ローカル ファイル システムからモデルを選択します", // Select a model from your local file system"
},
modelUrl: {
title: "モデルURL", // Model URL,
description: "公開 Web URL からモデルを読み込む", // Load your model from a public web URL"
},
predictTag: {
title: "予測タグ", // Predict Tag,
description: "予測にタグを自動的に含めるかどうか", // Whether or not to automatically include tags in predictions"
},
},
},
messages: {
loadingModel: "アクティブ ラーニング モデルを読み込んでいます...", // Loading active learning model...,
errorLoadModel: "アクティブ ラーニング モデルの読み込みエラー", // Error loading active learning model,
saveSuccess: "アクティブ ラーニング設定を保存しました", // Successfully saved active learning settings"
},
},
profile: {
settings: "プロファイル設定", // Profile Settings"
},
errors: {
unknown: {
title: "不明なエラー", // Unknown Error,
message: "アプリで不明なエラーが発生しました。", // The app encountered an unknown error. Please try again"
},
projectUploadError: {
title: "ファイルのアップロード エラー", // Error Uploading File,
message: "ファイルのアップロード中にエラーが発生しました。",
// There was an error uploading the file. Please verify the file is of the correct format and try again."
},
genericRenderError: {
title: "アプリケーションの読み込みエラー", // Error Loading Application,
message: "アプリケーションのレンダリング中にエラーが発生しました。",
// An error occured while rendering the application. Please try again"
},
projectInvalidSecurityToken: {
title: "プロジェクト ファイルの読み込みエラー", // Error loading project file,
message: "プロジェクトが参照するセキュリティ トークンが無効です。",
// The security token referenced by the project is invalid.
// Verify that the security token for the project has been set correctly within your application settings"
},
projectInvalidJson: {
title: "プロジェクト ファイルの解析エラー", // Error parsing project file,
message: "選択したプロジェクト ファイルに有効なJSONが含まれていません。",
// The selected project files does not contain valid JSON Please check the file any try again."
},
projectDeleteError: {
title: "プロジェクトの削除エラー", // Error deleting project,
message: "プロジェクトの削除中にエラーが発生しました。",
// An error occured while deleting the project.
// Validate the project file an security token exist and try again"
},
securityTokenNotFound: {
title: "プロジェクト ファイルの読み込みエラー", // Error loading project file,
message: "プロジェクトが参照するセキュリティ トークンが現在のアプリケーション設定に見つかりません。",
// The security token referenced by the project cannot be found in your current application settings.
// Verify the security token exists and try to reload the project."
},
canvasError: {
title: "キャンバスの読み込みエラー", // Error loading canvas,
message: "キャンバスのロード中にエラーが発生しました。プロジェクトのアセットを確認して、再試行してください。",
// There was an error loading the canvas, check the project's assets and try again."
},
importError: {
title: "V1 プロジェクトのインポート エラー", // Error importing V1 project,
message: "V1 プロジェクトのインポート中にエラーが発生しました。",
// There was an error importing the V1 project. Check the project file and try again"
},
pasteRegionTooBigError: {
title: "領域の貼り付けエラー", // Error pasting region,
message: "このアセットに対して領域が大きすぎます。別のリージョンをコピーしてください",
// Region too big for this asset. Try copying another region"
},
exportFormatNotFound: {
title: "プロジェクトのエクスポート エラー", // Error exporting project,
message: "プロジェクトにエクスポート形式がありません。",
// Project is missing export format. Please select an export format in the export setting page."
},
activeLearningPredictionError: {
title: "アクティブ ラーニングのエラー", // Active Learning Error,
message: "現在のアセットの領域を予測中にエラーが発生しました。",
// An error occurred while predicting regions in the current asset.
// Please verify your active learning configuration and try again"
},
},
};

Просмотреть файл

@ -0,0 +1,512 @@
import { IAppStrings } from "../strings";
/**
* App Strings for Korean language from Google Translate
*/
export const korean: IAppStrings = {
appName: "비주얼 객체 태깅 도구", // Visual Object Tagging Tool,
common: {
displayName: "프로젝트 이름", // Display Name,
description: "설명", // Description,
submit: "제출", // Submit,
cancel: "취소", // Cancel,
save: "저장", // Save,
delete: "삭제", // Delete,
provider: "공급자", // Provider,
homePage: "홈페이지", // Home Page"
},
titleBar: {
help: "도움", // Help,
minimize: "최소화", // Minimize,
maximize: "최대화", // Maximize,
restore: "되돌리기", // Restore,
close: "닫기", // Close"
},
homePage: {
newProject: "새로운 프로젝트", // New Project,
openLocalProject: {
title: "로컬 프로젝트 열기", // Open Local Project"
},
openCloudProject: {
title: "클라우드 프로젝트 열기", // Open Cloud Project,
selectConnection: "Connection 선택", // Select a Connection
},
recentProjects: "최근 프로젝트", // Recent Projects,
deleteProject: {
title: "프로젝트 삭제", // Delete Project,
confirmation: "프로젝트를 삭제 하시겠습니까?", // Are you sure you want to delete project
},
importProject: {
title: "프로젝트 가져 오기", // Import Project,
confirmation: "${project.file.name} 프로젝트 설정을 v2 형식으로 수정 하시겠습니까? 수정하시기 전에 프로젝트 파일을 백업해두시기 바랍니다.",
// Are you sure you want to conver project ${project.file.name} project settings to v2 format?
// We recommend you backup the project file first."
},
messages: {
deleteSuccess: "${project.name}을 삭제했습니다", // Successfully deleted ${project.name}"
},
},
appSettings: {
title: "애플리케이션 설정", // Application Settings,
storageTitle: "저장소 설정", // Storage Settings,
uiHelp: "설정이 저장된 위치", // Where your settings are stored,
save: "설정 저장", // Save Settings,
securityToken: {
name: {
title: "이름", // Name
},
key: {
title: "키", // Key
},
},
securityTokens: {
title: "보안 토큰", // Security Tokens,
description: "보안 토큰은 프로젝트 구성 내에서 중요한 데이터를 암호화하는 데 사용됩니다",
// Security tokens are used to encryp sensitive data within your project configuration"
},
version: {
description: "버전:", // Version"
},
commit: "커밋 SHA", // Commit SHA,
devTools: {
description: "이슈 진단을 돕기 위한 개발자 도구 열기", // Open application developer tools to help diagnose issues,
button: "개발자 도구 전환", // Toggle Developer Tools
},
reload: {
description: "모든 변경사항을 버리고 애플리케이션을 재시작 합니다", // Reload the app discarding all current changes,
button: "애플리케이션 새로고침", // Refresh Application
},
messages: {
saveSuccess: "애플리케이션 설정이 성공적으로 저장되었습니다", // Successfully saved application settings
},
},
projectSettings: {
title: "프로젝트 설정", // Project Settings,
securityToken: {
title: "보안 토큰", // Security Token,
description: "프로젝트 파일 내에서 중요한 데이터를 암호화하는 데 사용", // Used to encrypt sensitive data within project file
},
save: "프로젝트 저장", // Save Project,
sourceConnection: {
title: "소스 연결", // Source Connection,
description: "Asset 저장 경로", // Where to load assets from
},
targetConnection: {
title: "대상 연결", // Target Connection,
description: "프로젝트 및 내 보낸 데이터를 저장할 위치", // Where to save the project and exported data
},
videoSettings: {
title: "비디오 설정", // Video Settings,
description: "태그 지정을 위해 프레임을 추출하는 비율", // The rate at which frames are extracted for tagging
frameExtractionRate: "프레임 추출 속도 (비디오 초당 프레임)", // Frame Extraction Rate (frames per a video second)
},
addConnection: "연결 추가", // Add Connection,
messages: {
saveSuccess: "${project.name} 프로젝트 설정을 성공적으로 저장했습니다", // Successfully saved ${project.name} project settings
},
},
projectMetrics: {
title: "프로젝트 매트릭", // Project Metrics,
assetsSectionTitle: "Asset", // Assets,
totalAssetCount: "총 Asset", // Total Assets,
visitedAssets: "검토한 Asset (${count})", // Visited Assets (${count}),
taggedAssets: "태그된 Asset (${count})", // Tagged Assets (${count}),
nonTaggedAssets: "태그가 없는 Asset (${count})", // Not Tagged Assets (${count}),
nonVisitedAssets: "검토하지 않은 Asset (${count})", // Not Visited Assets (${count}),
tagsSectionTitle: "태그", // Tags & Labels,
totalRegionCount: "태그된 지역 수", // Total Tagged Regions,
totalTagCount: "태그 숫자", // Total Tags,
avgTagCountPerAsset: "Asset 당 평균 태그 숫자", // Average tags per asset"
},
tags: {
title: "태그", // Tags,
placeholder: "새 태그 추가", // Add new tag,
editor: "태그 편집기", // Tags Editor,
modal: {
name: "태그 이름", // Tag Name,
color: "태그 색상", // Tag Color"
},
colors: {
white: "하얀색", // White,
gray: "회색", // Gray,
red: "빨간색", // Red,
maroon: "밤색", // Maroon,
yellow: "노랑색", // Yellow,
olive: "올리브색", // Olive,
lime: "라임색", // Lime,
green: "초록색", // Green,
aqua: "아쿠아", // Aqua,
teal: "물오리", // Teal,
blue: "파랑색", // Blue,
navy: "군청색", // Navy,
fuschia: "푸시아", // Fuschia,
purple: "보라색", // Purple"
},
warnings: {
existingName: "태그 이름이 이미 존재합니다. 다른 이름을 입력하십시오", // Tag name already exists. Choose another name,
emptyName: "빈 태그 이름을 가질 수 없습니다", // Cannot have an empty tag name,
unknownTagName: "알 수 없는 태그 이름", // Unknown"
},
toolbar: {
add: "새 태그 추가", // Add new tag,
search: "태크 검색", // Search tags,
edit: "태그 편집", // Edit tag,
lock: "태그 잠금", // Lock tag,
moveUp: "태그를 위로 이동", // Move tag up,
moveDown: "태그를 아래로 이동", // Move tag down,
delete: "태그 삭제", // Delete tag"
},
},
connections: {
title: "연결 설정", // Connections,
details: "설명", // Connection Details,
settings: "설정", // Connection Settings,
instructions: "편집 할 연결 정보를 선택하십시오", // Please select a connection to edit,
save: "저장", // Save Connection,
messages: {
saveSuccess: "${connection.name}을 성공적으로 저장했습니다", // Successfully saved ${connection.name},
deleteSuccess: "${connection.name}을 삭제했습니다.", // Successfully deleted ${connection.name}"
},
imageCorsWarning: "경고 : 웹 브라우저에서 VoTT를 사용하는 경우 CORS (Cross Origin Resource Sharing) " +
"제한으로 인해 Bing Image Search의 일부 정보가 제대로 내보내지지 않을 수 있습니다.",
// Warning: When using VoTT in a Web browser, some assets from Bing Image Search may no export
// correctly due to CORS (Cross Origin Resource Sharing) restrictions.",
blobCorsWarning: "경고 : 소스 또는 대상 연결로 사용하려면, Azure Blob Storage 계정에서 CORS(Cross Domain Resource Sharing) " +
"설정을 활성화 해야 합니다. CORS 설정에 대한 자세한 정보는 {0}에서 찾을 수 있습니다.",
// Warning: CORS (Cross Domain Resource Sharing) must be enabled on the Azure Blob Storage account, in order
// to use i as a source or target connection. More information on enabling CORS can be found in the {0}",
azDocLinkText: "Azure 설명서.", // Azure Documentation.,
providers: {
azureBlob: {
title: "Azure Blob 저장소", // Azure Blob Storage,
description: "",
accountName: {
title: "계정 이름", // Account Name,
description: "",
},
containerName: {
title: "컨테이너 이름", // Container Name,
description: "",
},
sas: {
title: "SAS", // SAS,
description: "Blob Storage 계정을 인증하는 데 사용되는 공유 액세스 서명",
// Shared access signature used to authenticate to the blob storage account"
},
createContainer: {
title: "컨테이너 만들기", // Create Container,
description: "Blob 컨테이너가 없으면 새로 생성합니다.",
// Creates the blob container if it does not already exist"
},
},
bing: {
title: "Bing 이미지 검색", // Bing Image Search,
options: "Bing 이미지 검색 옵션", // Bing Image Search Options,
apiKey: "API 키", // API Key,
query: "쿼리", // Query,
aspectRatio: {
title: "종횡비", // Aspect Ratio,
all: "모두", // All,
square: "정사각형", // Square,
wide: "넓은", // Wide,
tall: "긴", // Tall"
},
},
local: {
title: "로컬 파일 시스템", // Local File System,
folderPath: "경로", // Folder Path,
selectFolder: "폴더 선택", // Select Folder,
chooseFolder: "선택", // Choose Folder"
},
},
},
editorPage: {
width: "너비", // Width,
height: "높이", // Height,
tagged: "태그", // Tagged,
visited: "방문", // Visited,
toolbar: {
select: "선택 (V)", // Select (V),
pan: "팬", // Pan,
drawRectangle: "사각형 그리기", // Draw Rectangle,
drawPolygon: "다각형 그리기", // Draw Polygon,
copyRectangle: "사각형 복사", // Copy Rectangle,
copy: "영역 복사", // Copy Regions,
cut: "영역 잘라내기", // Cut Regions,
paste: "영역 붙여 넣기", // Paste Regions,
removeAllRegions: "모든 지역 제거", // Remove All Regions,
previousAsset: "이전 Asset", // Previous Asset,
nextAsset: "다음 Asset", // Next Asset,
saveProject: "프로젝트 저장", // Save Project,
exportProject: "프로젝트 내보내기", // Export Project,
activeLearning: "Active Learning", // Active Learning"
},
videoPlayer: {
previousTaggedFrame: {
tooltip: "이전 태그 된 프레임", // Previous Tagged Frame"
},
nextTaggedFrame: {
tooltip: "다음 태그 된 프레임", // Next Tagged Frame"
},
previousExpectedFrame: {
tooltip: "이전 프레임", // Previous Frame"
},
nextExpectedFrame: {
tooltip: "다음 프레임", // Next Frame"
},
},
help: {
title: "도움말", // Toggle Help Menu,
escape: "나가기", // Escape Help Menu"
},
assetError: "Asset을 불러올 수 없습니다", // Unable to load asset,
tags: {
hotKey: {
apply: "단축키로 태그 적용", // Apply Tag with Hot Key,
lock: "단축키가 있는 태그 잠금", // Lock Tag with Hot Key"
},
rename: {
title: "태그 이름 바꾸기", // Rename Tag,
confirmation: "이 태그의 이름을 바꾸시겠습니까? 모든 Asset에서 이름이 변경됩니다",
// Are you sure you want to rename this tag? It will be renamed throughout all assets"
},
delete: {
title: "태그 삭제", // Delete Tag,
confirmation: "이 태그를 삭제 하시겠습니까? 모든 Asset 및 태그가 유일한 지역 인 모든 지역에서 삭제됩니다.",
// Are you sure you want to delete this tag? It will be deleted throughout all assets
// and any regions where this is the only tag will also be deleted"
},
},
canvas: {
removeAllRegions: {
title: "모든 지역 제거", // Remove All Regions,
confirmation: "모든 지역을 삭제 하시겠습니까?", // Are you sure you want to remove all regions"
},
},
messages: {
enforceTaggedRegions: {
title: "유효하지 않은 지역이 감지되었습니다.", // Invalid region(s) detected,
description: "1 개 이상의 지역이 태그되어야 합니다. 다음 작업을 계속 진행하기 위해 모든 지역에 태그가 지정되어 있는지 확인하십시오.",
// 1 or more regions have not been tagged. Ensure all regions ar tagged before continuing to next asset"
},
},
},
export: {
title: "내보내기", // Export,
settings: "내보내기 설정", // Export Settings,
saveSettings: "내보내기 설정 저장", // Save Export Settings,
providers: {
common: {
properties: {
assetState: {
title: "Asset 상태", // Asset State,
description: "내보내기에 포함 할 Asset", // Which assets to include in the export,
options: {
all: "모든 Asset", // All Assets,
visited: "방문한 Asset만", // Only Visited Assets,
tagged: "태그된 Asset만", // Only tagged Assets"
},
},
testTrainSplit: {
title: "테스트용 / 학습용 분할", // Test / Train Split,
description: "내보내는 데이터에 테스트용 / 학습용 분할",
// The test train split to use for exported data"
},
includeImages: {
title: "이미지 포함", // Include Images,
description: "대상 연결에 이진 이미지 Asset을 포함할지 여부",
// Whether or not to include binary image assets in target connection"
},
},
},
vottJson: {
displayName: "VoTT JSON", // VoTT JSON
},
azureCV: {
displayName: "Azure Custom Vision 서비스", // Azure Custom Vision Service,
regions: {
australiaEast: "호주 동부", // Australia East,
centralIndia: "중앙 인도", // Central India,
eastUs: "미국 동부", // East US,
eastUs2: "미국 동부 2", // East US 2,
japanEast: "일본 동부", // Japan East,
northCentralUs: "미국 중북부", // North Central US,
northEurope: "북유럽", // North Europe,
southCentralUs: "미국 중남부", // South Central US,
southeastAsia: "동남아시아", // Southeast Asia,
ukSouth: "영국 남부", // UK South,
westUs2: "미국 서부 2", // West US 2,
westEurope: "서유럽", // West Europe"
},
properties: {
apiKey: {
title: "API 키", // API Key"
},
region: {
title: "지역", // Region,
description: "서비스가 배포 된 Azure 지역", // The Azure region where your service is deployed"
},
classificationType: {
title: "분류 유형", // Classification Type,
options: {
multiLabel: "이미지 당 여러 태그", // Multiple tags per image,
multiClass: "이미지 당 단일 태그", // Single tag per image"
},
},
name: {
title: "프로젝트 이름", // Project Name"
},
description: {
title: "설명", // Project Description"
},
domainId: {
title: "도메인", // Domain"
},
newOrExisting: {
title: "신규 또는 기존 프로젝트", // New or Existing Project,
options: {
new: "새로운 프로젝트", // New Project,
existing: "기존 프로젝트", // Existing Project"
},
},
projectId: {
title: "프로젝트 이름", // Project Name"
},
projectType: {
title: "프로젝트 유형", // Project Type,
options: {
classification: "분류", // Classification,
objectDetection: "물체 감지", // Object Detection"
},
},
},
},
tfRecords: {
displayName: "TensorFlow 기록", // Tensorflow Record"
},
pascalVoc: {
displayName: "Pascal VOC", // Pascal VOC,
exportUnassigned: {
title: "할당되지 않은 태그 내보내기", // Export Unassigned,
description: "내보내는 데이터에 할당되지 않은 태그를 포함할지 여부",
// Whether or not to include unassigned tags in exported data"
},
},
cntk: {
displayName: "Microsoft Cognitive ToolkitCNTK", // Microsoft Cognitive Toolkit (CNTK)"
},
csv: {
displayName: "쉼표로 구분 된 값CSV", // Comma Separated Values (CSV)"
},
},
messages: {
saveSuccess: "내보내기 설정이 성공적으로 저장되었습니다", // Successfully saved export settings"
},
},
activeLearning: {
title: "Active Learning", // Active Learning,
form: {
properties: {
modelPathType: {
title: "모델 제공자", // Model Provider,
description: "학습 모델을 불러올 위치", // Where to load the training model from,
options: {
preTrained: "미리 학습된 Coco SSD", // Pre-trained Coco SSD,
customFilePath: "사용자 정의 (파일 경로)", // Custom (File path),
customWebUrl: "사용자 정의 (URL)", // Custom (Url)"
},
},
autoDetect: {
title: "자동 감지", // Auto Detect,
description: "Asset 간을 탐색 할 때 자동 예측 여부",
// Whether or not to automatically make predictions as you navigate between assets"
},
modelPath: {
title: "모델 경로", // Model path,
description: "로컬 파일 시스템에서 모델을 선택하십시오.", // Select a model from your local file system"
},
modelUrl: {
title: "모델 URL", // Model URL,
description: "URL에서 모델 불러오기", // Load your model from a public web URL"
},
predictTag: {
title: "태그 예측", // Predict Tag,
description: "예측에 태그를 자동으로 포함할지 여부", // Whether or not to automatically include tags in predictions"
},
},
},
messages: {
loadingModel: "Active Learning 모델 불러오는 중 ...", // Loading active learning model...,
errorLoadModel: "Active Learning 모델을 불러오는 중 오류가 발생했습니다", // Error loading active learning model,
saveSuccess: "Active Learning 모델 설정을 성공적으로 저장했습니다", // Successfully saved active learning settings"
},
},
profile: {
settings: "프로필 설정", // Profile Settings"
},
errors: {
unknown: {
title: "알 수 없는 오류", // Unknown Error,
message: "애플리케이션에 알 수 없는 오류가 발생했습니다. 다시 시도하십시오.", // The app encountered an unknown error. Please try again"
},
projectUploadError: {
title: "파일 업로드 오류", // Error Uploading File,
message: "파일을 업로드하는 중에 오류가 발생했습니다. 파일이 올바른 형식인지 확인한 후 다시 시도하십시오.",
// There was an error uploading the file. Please verify the file is of the correct format and try again."
},
genericRenderError: {
title: "응용 프로그램 로딩 오류", // Error Loading Application,
message: "응용 프로그램을 렌더링하는 중에 오류가 발생했습니다. 다시 시도하십시오",
// An error occured while rendering the application. Please try again"
},
projectInvalidSecurityToken: {
title: "프로젝트 파일을 로드하는 중 오류가 발생했습니다", // Error loading project file,
message: "프로젝트에서 참조한 보안 토큰이 유효하지 않습니다.응용 프로그램 설정 내에서 프로젝트의 보안 토큰이 올바르게 설정되었는지 확인하십시오",
// The security token referenced by the project is invalid.
// Verify that the security token for the project has been set correctly within your application settings"
},
projectInvalidJson: {
title: "프로젝트 파일 파싱 오류", // Error parsing project file,
message: "선택한 프로젝트 파일에 유효한 JSON이 포함되어 있지 않습니다. 파일을 다시 확인하십시오.",
// The selected project files does not contain valid JSON Please check the file any try again."
},
projectDeleteError: {
title: "프로젝트 삭제 오류", // Error deleting project,
message: "프로젝트를 삭제하는 중에 오류가 발생했습니다. 프로젝트 파일에 보안 토큰이 존재하는지 확인한 후 다시 시도하십시오.",
// An error occured while deleting the project.
// Validate the project file an security token exist and try again"
},
securityTokenNotFound: {
title: "프로젝트 파일을 로드하는 중 오류가 발생했습니다", // Error loading project file,
message: "프로젝트가 참조하는 보안 토큰을 현재 애플리케이션 설정에서 찾을 수 없습니다. 보안 토큰이 있는지 확인하고 프로젝트를 다시로드하십시오.",
// The security token referenced by the project cannot be found in your current application settings.
// Verify the security token exists and try to reload the project."
},
canvasError: {
title: "캔버스 불러 오기 오류", // Error loading canvas,
message: "캔버스를 로드하는 중에 오류가 발생했습니다. 프로젝트 Asset을 확인한 후 다시 시도하십시오.",
// There was an error loading the canvas, check the project's assets and try again."
},
importError: {
title: "V1 프로젝트 가져 오기 오류", // Error importing V1 project,
message: "V1 프로젝트를 가져 오는 중에 오류가 발생했습니다. 프로젝트 파일을 확인하고 다시 시도하십시오.",
// There was an error importing the V1 project. Check the project file and try again"
},
pasteRegionTooBigError: {
title: "지역 붙여 넣기 오류", // Error pasting region,
message: "이 Asset에 비해 지역이 너무 큽니다. 다른 지역을 복사 해보십시오.",
// Region too big for this asset. Try copying another region"
},
exportFormatNotFound: {
title: "프로젝트 내보내기 오류", // Error exporting project,
message: "프로젝트에 내보내기 형식이 없습니다. 내보내기 설정 페이지에서 내보내기 형식을 선택하십시오.",
// Project is missing export format. Please select an export format in the export setting page."
},
activeLearningPredictionError: {
title: "Active Learning 오류", // Active Learning Error,
message: "현재 Asset의 지역을 예측하는 동안 오류가 발생했습니다. Active Learning 구성을 확인하고 다시 시도하십시오",
// An error occurred while predicting regions in the current asset.
// Please verify your active learning configuration and try again"
},
},
};

Просмотреть файл

@ -0,0 +1,509 @@
import { IAppStrings } from "../strings";
/**
* App Strings for Simplified Chinese (zh-ch)
*/
export const chinese: IAppStrings = {
appName: "视觉对象标记工具", // Visual Object Tagging Tool
common: {
displayName: "显示名称", // Display Name
description: "描述", // Description
submit: "提交", // Submit
cancel: "取消", // Cancel
save: "保存", // Save
delete: "删除", // Delete
provider: "提供者", // Provider
homePage: "主页", // Home Page
},
titleBar: {
help: "帮助", // Help
minimize: "最小化", // Minimize
maximize: "最大化", // Maximize
restore: "复原", // Restore
close: "关闭", // Close
},
homePage: {
newProject: "新项目", // New Project
openLocalProject: {
title: "打开本地项目", // Open Local Project
},
openCloudProject: {
title: "打开云端项目", // Open Cloud Project
selectConnection: "选择一个连接", // Select a Connection
},
recentProjects: "最近的项目", // Recent Projects
deleteProject: {
title: "删除项目", // Delete Project
confirmation: "确定要删除项目吗", // Are you sure you want to delete project
},
importProject: {
title: "导入项目", // Import Project
confirmation: "您确定要将项目${project.file.name}设置转换为v2格式吗我们建议您首先备份项目文件。",
// Are you sure you want to convert project ${project.file.name} project settings to v2 format?
// We recommend you backup the project file first.
},
messages: {
deleteSuccess: "已成功删除${project.name}", // Successfully deleted ${project.name}
},
},
appSettings: {
title: "应用程序设置", // Application Settings
storageTitle: "储存设置", // Storage Settings
uiHelp: "您的设置储存在哪里", // Where your settings are stored
save: "保存设置", // Save Settings
securityToken: {
name: {
title: "名称", // Name
},
key: {
title: "密钥", // Key
},
},
securityTokens: {
title: "安全令牌", // Security Tokens
description: "安全令牌用于加密项目配置中的敏感数据",
// Security tokens are used to encrypt sensitive data within your project configuration
},
version: {
description: "版本:", // Version:
},
commit: "提交SHA", // Commit SHA
devTools: {
description: "打开应用程序开发者工具以帮助诊断问题", // Open application developer tools to help diagnose issues
button: "切换至开发者工具", // Toggle Developer Tools
},
reload: {
description: "重新加载应用,放弃当前所有更改", // Reload the app discarding all current changes
button: "刷新应用", // Refresh Application
},
messages: {
saveSuccess: "成功保存应用程序设置", // Successfully saved application settings
},
},
projectSettings: {
title: "项目设定", // Project Settings
securityToken: {
title: "安全令牌", // Security Token
description: "用于加密项目文件中的敏感数据", // Used to encrypt sensitive data within project files
},
save: "保存项目", // Save Project
sourceConnection: {
title: "源连接", // Source Connection
description: "从何处加载素材", // Where to load assets from
},
targetConnection: {
title: "目标连接", // Target Connection
description: "在哪里保存项目和导出数据", // Where to save the project and exported data
},
videoSettings: {
title: "视频设定", // Video Settings
description: "提取帧以进行标记的速率", // The rate at which frames are extracted for tagging.
frameExtractionRate: "帧提取率(每视频每秒的帧数)", // Frame Extraction Rate (frames per a video second)
},
addConnection: "添加连接", // Add Connection
messages: {
saveSuccess: "成功保存${project.name}项目设置", // Successfully saved ${project.name} project settings
},
},
projectMetrics: {
title: "项目指标", // Project Metrics
assetsSectionTitle: "素材", // Assets
totalAssetCount: "素材总数", // Total Assets
visitedAssets: "已访问素材(${count}", // Visited Assets (${count})
taggedAssets: "已标记素材(${count}", // Tagged Assets (${count})
nonTaggedAssets: "未标记素材(${count}", // Not Tagged Assets (${count})
nonVisitedAssets: "未访问素材(${count}", // Not Visited Assets (${count})
tagsSectionTitle: "标记与标签", // Tags & Labels
totalRegionCount: "标记区域总数", // Total Tagged Regions
totalTagCount: "标签总数", // Total Tags
avgTagCountPerAsset: "每个素材的平均标签数", // Average tags per asset
},
tags: {
title: "标签", // Tags
placeholder: "添加标签", // Add new tag
editor: "编辑标签", // Tags Editor
modal: {
name: "标签名称", // Tag Name
color: "标签颜色", // Tag Color
},
colors: {
white: "白色", // White
gray: "灰色", // Gray
red: "红色", // Red
maroon: "栗色", // Maroon
yellow: "黄色", // Yellow
olive: "橄榄色", // Olive
lime: "青色", // Lime
green: "绿色", // Green
aqua: "浅绿色", // Aqua
teal: "蓝绿色", // Teal
blue: "蓝色", // Blue
navy: "海军蓝色", // Navy
fuschia: "紫红色", // Fuschia
purple: "紫色", // Purple
},
warnings: {
existingName: "标签名称已存在。请选择另一个名字", // Tag name already exists. Choose another name
emptyName: "标签名称不能为空", // Cannot have an empty tag name
unknownTagName: "未知", // Unknown
},
toolbar: {
add: "添加标签", // Add new tag
search: "搜索标签", // Search tags
edit: "编辑标签", // Edit tag
lock: "锁定标签", // Lock tag
moveUp: "向上移动标签", // Move tag up
moveDown: "向下移动标签", // Move tag down
delete: "删除标签", // Delete tag
},
},
connections: {
title: "连接数", // Connections
details: "连接详细信息", // Connection Details
settings: "连接设定", // Connection Settings
instructions: "请选择一个连接进行编辑", // Please select a connection to edit
save: "保存连接", // Save Connection
messages: {
saveSuccess: "已成功保存${connection.name}", // Successfully saved ${connection.name}
deleteSuccess: "已成功删除${connection.name}", // Successfully deleted ${connection.name}
},
imageCorsWarning: "警告在Web浏览器中使用VoTT时由于CORS跨源资源共享限制来自Bing Image Search的某些素材可能无法正确导出。",
// Warning: When using VoTT in a Web browser, some assets from Bing Image Search may not export correctly
// due to CORS (Cross Origin Resource Sharing) restrictions.
blobCorsWarning: "警告必须在Azure Blob存储帐户上启用CORS跨域资源共享才能将其用作源或目标连接。 {0}中提供了有关启用CORS的更多信息。",
// Warning: CORS (Cross Domain Resource Sharing) must be enabled on the Azure Blob Storage account,
// in order to use it as a source or target connection.
// More information on enabling CORS can be found in the {0}
azDocLinkText: "Azure文档", // Azure Documentation.
providers: {
azureBlob: {
title: "Azure Blob存储", // Azure Blob Storage
description: "",
accountName: {
title: "用户名", // Account Name
description: "",
},
containerName: {
title: "容器名称", // Container Name
description: "",
},
sas: {
title: "SAS", // SAS
description: "用于验证Blob存储帐户的共享访问签名",
// Shared access signature used to authenticate to the blob storage account
},
createContainer: {
title: "创建容器", // Create Container
description: "创建blob容器如果尚不存在",
// Creates the blob container if it does not already exist
},
},
bing: {
title: "必应图片搜索", // Bing Image Search
options: "必应图像搜索选项", // Bing Image Search Options
apiKey: "API密钥", // API Key
query: "查询", // Query
aspectRatio: {
title: "长宽比", // Aspect Ratio
all: "所有", // All
square: "正方形", // Square
wide: "宽", // Wide
tall: "高", // Tall
},
},
local: {
title: "本地文件系统", // Local File System
folderPath: "文件夹路径", // Folder Path
selectFolder: "选择文件夹", // Select Folder
chooseFolder: "选择文件夹", // Choose Folder
},
},
},
editorPage: {
width: "宽度", // Width
height: "高度", // Height
tagged: "已标记", // Tagged
visited: "已访问", // Visited
toolbar: {
select: "选择[V]", // Select (V)
pan: "泛", // Pan
drawRectangle: "绘制矩形", // Draw Rectangle
drawPolygon: "绘制多边形", // Draw Polygon
copyRectangle: "复制矩形", // Copy Rectangle
copy: "复制区域", // Copy Regions
cut: "剪切区域", // Cut Regions
paste: "粘贴区域", // Paste Regions
removeAllRegions: "删除所有区域", // Remove All Regions
previousAsset: "以前的素材", // Previous Asset
nextAsset: "下一项素材", // Next Asset
saveProject: "保存项目", // Save Project
exportProject: "导出项目", // Export Project
activeLearning: "主动学习", // Active Learning
},
videoPlayer: {
previousTaggedFrame: {
tooltip: "上一个已标记的帧", // Previous Tagged Frame
},
nextTaggedFrame: {
tooltip: "下一个已标记的帧", // Next Tagged Frame
},
previousExpectedFrame: {
tooltip: "上一帧", // Previous Frame
},
nextExpectedFrame: {
tooltip: "下一帧", // Next Frame
},
},
help: {
title: "切换帮助菜单", // Toggle Help Menu
escape: "退出帮助菜单", // Escape Help Menu
},
assetError: "无法加载素材", // Unable to load asset
tags: {
hotKey: {
apply: "使用快捷键应用标签", // Apply Tag with Hot Key
lock: "使用快捷键锁定标签", // Lock Tag with Hot Key
},
rename: {
title: "重新命名标签", // Rename Tag
confirmation: "您确定要重新命名此标签吗?它将在所有素材中被重新命名",
// Are you sure you want to rename this tag? It will be renamed throughout all assets
},
delete: {
title: "删除标签", // Delete Tag
confirmation: "您确定要删除此标签吗?它将在所有素材中被删除,并且仅使用此标签标记的任何区域也将被删除",
// Are you sure you want to delete this tag? It will be deleted throughout all assets
// and any regions where this is the only tag will also be deleted
},
},
canvas: {
removeAllRegions: {
title: "删除所有区域", // Remove All Regions
confirmation: "您确定要删除所有区域吗?", // Are you sure you want to remove all regions?
},
},
messages: {
enforceTaggedRegions: {
title: "检测到无效的区域", // Invalid region(s) detected
description: "1个或多个区域尚未被标记。在继续下一个素材之前请确保所有区域均已标记。",
// 1 or more regions have not been tagged.
// Ensure all regions are tagged before continuing to next asset.
},
},
},
export: {
title: "导出", // Export
settings: "导出设置", // Export Settings
saveSettings: "保存导出设置", // Save Export Settings
providers: {
common: {
properties: {
assetState: {
title: "素材状态", // Asset State
description: "导出中包括哪些素材", // Which assets to include in the export
options: {
all: "所有素材", // All Assets
visited: "仅已访问素材", // Only Visited Assets
tagged: "仅已标记素材", // Only tagged Assets
},
},
testTrainSplit: {
title: "测试/训练用数据分离", // Test / Train Split
description: "导出时分离测试/训练用数据", // The test train split to use for exported data
},
includeImages: {
title: "包含图片", // Include Images
description: "是否在目标连接中包括二值化图像素材",
// Whether or not to include binary image assets in target connection
},
},
},
vottJson: {
displayName: "VoTT JSON", // VoTT JSON
},
azureCV: {
displayName: "Azure自定义视觉服务", // Azure Custom Vision Service
regions: {
australiaEast: "澳大利亚东部", // Australia East
centralIndia: "印度中部", // Central India
eastUs: "美国东部", // East US
eastUs2: "美国东部2", // East US 2
japanEast: "日本东部", // Japan East
northCentralUs: "美国中北部", // North Central US
northEurope: "欧州北部", // North Europe
southCentralUs: "美国中南部", // South Central US
southeastAsia: "东南亚", // Southeast Asia
ukSouth: "英国南部", // UK South
westUs2: "美国西部2", // West US 2
westEurope: "欧州西部", // West Europe
},
properties: {
apiKey: {
title: "API密钥", // API Key
},
region: {
title: "区域", // Region
description: "部署服务的Azure区域", // The Azure region where your service is deployed
},
classificationType: {
title: "分类类型", // Classification Type
options: {
multiLabel: "每个图像多个标签", // Multiple tags per image
multiClass: "每个图像一个标签", // Single tag per image
},
},
name: {
title: "项目名", // Project Name
},
description: {
title: "项目简介", // Project Description
},
domainId: {
title: "域", // Domain
},
newOrExisting: {
title: "新项目或现有项目", // New or Existing Project
options: {
new: "新项目", // New Project
existing: "现有项目", // Existing Project
},
},
projectId: {
title: "项目名", // Project Name
},
projectType: {
title: "项目类型", // Project Type
options: {
classification: "分类", // Classification
objectDetection: "物体识别", // Object Detection
},
},
},
},
tfRecords: {
displayName: "Tensorflow记录", // Tensorflow Records
},
pascalVoc: {
displayName: "Pascal VOC", // Pascal VOC
exportUnassigned: {
title: "导出未分配", // Export Unassigned
description: "是否在导出的数据中包括未被分配的标签", // Whether or not to include unassigned tags in exported data
},
},
cntk: {
displayName: "Microsoft Cognitive ToolkitCNTK)", // Microsoft Cognitive Toolkit (CNTK)
},
csv: {
displayName: "逗号分隔值 (CSV)", // Comma Separated Values (CSV)
},
},
messages: {
saveSuccess: "成功保存导出设置", // Successfully saved export settings
},
},
activeLearning: {
title: "主动学习", // Active Learning
form: {
properties: {
modelPathType: {
title: "模型提供者", // Model Provider
description: "从何处加载训练模型", // Where to load the training model from
options: {
preTrained: "预先训练 Coco SSD", // Pre-trained Coco SSD
customFilePath: "自定义(文件路径)", // Custom (File path)
customWebUrl: "自定义 (URL)", // Custom (Url)
},
},
autoDetect: {
title: "自动识别", // Auto Detect
description: "在素材之间导航时是否自动进行预测",
// Whether or not to automatically make predictions as you navigate between assets
},
modelPath: {
title: "模型路径", // Model path
description: "从本地文件系统中选择模型", // Select a model from your local file system
},
modelUrl: {
title: "模型 URL", // Model URL
description: "从公共网址加载模型", // Load your model from a public web URL
},
predictTag: {
title: "预测标签", // Predict Tag
description: "是否在预测中自动包含标签", // Whether or not to automatically include tags in predictions
},
},
},
messages: {
loadingModel: "正在加载主动学习模型...", // Loading active learning model...
errorLoadModel: "加载主动学习模型时出错", // Error loading active learning model
saveSuccess: "成功保存了主动学习设置", // Successfully saved active learning settings
},
},
profile: {
settings: "个人资料设置", // Profile Settings
},
errors: {
unknown: {
title: "未知错误", // Unknown Error
message: "该应用程序遇到未知错误。请重试。", // The app encountered an unknown error. Please try again.
},
projectUploadError: {
title: "上传文件时出错", // Error Uploading File
message: "上传文件时出错。请确认文件格式正确,然后重试。",
// There was an error uploading the file. Please verify the file is of the correct format and try again.
},
genericRenderError: {
title: "加载应用程序时出错", // Error Loading Application
message: "呈现应用程序时发生错误。请重试",
// An error occured while rendering the application. Please try again
},
projectInvalidSecurityToken: {
title: "加载项目文件时出错", // Error loading project file
message: "项目引用的安全令牌无效。请验证是否在您的应用程序设置中正确设置了项目的安全令牌",
// The security token referenced by the project is invalid.
// Verify that the security token for the project has been set correctly within your application settings
},
projectInvalidJson: {
title: "解析项目文件时出错", // Error parsing project file
message: "所选的项目文件不包含有效的JSON。请检查该文件, 然后重试。",
// The selected project files does not contain valid JSON. Please check the file any try again.
},
projectDeleteError: {
title: "删除项目时出错", // Error deleting project
message: "删除项目时发生错误。验证项目文件和安全令牌是否存在,然后重试",
// An error occured while deleting the project.
// Validate the project file and security token exist and try again
},
securityTokenNotFound: {
title: "加载项目文件时出错", // Error loading project file
message: "在当前的应用程序设置中找不到该项目引用的安全令牌。验证安全令牌是否存在,然后尝试重新加载项目。",
// The security token referenced by the project cannot be found in your current application settings.
// Verify the security token exists and try to reload the project.
},
canvasError: {
title: "加载画布时出错", // Error loading canvas
message: "加载画布时发生错误,请检查项目的素材,然后重试。",
// There was an error loading the canvas, check the project's assets and try again.
},
importError: {
title: "导入V1项目时出错", // Error importing V1 project
message: "导入V1项目时出错。检查项目文件然后重试",
// There was an error importing the V1 project. Check the project file and try again
},
pasteRegionTooBigError: {
title: "粘贴区域时出错", // Error pasting region
message: "区域对于该素材过大。请尝试复制其他区域", // Region too big for this asset. Try copying another region
},
exportFormatNotFound: {
title: "导出项目时出错", // Error exporting project
message: "项目缺少导出格式。请在导出设置页面中选择一种导出格式。",
// Project is missing export format. Please select an export format in the export setting page.
},
activeLearningPredictionError: {
title: "主动学习错误", // Active Learning Error
message: "预测当前素材中的区域时发生错误。请验证您的主动学习配置,然后重试",
// An error occurred while predicting regions in the current asset.
// Please verify your active learning configuration and try again
},
},
};

Просмотреть файл

@ -0,0 +1,514 @@
import { IAppStrings } from "../strings";
/**
* App Strings for Traditional Chinese (zh-tw)
*/
export const chinesetw: IAppStrings = {
appName: "VOTT視覺物件標記工具", // Visual Object Tagging Tool
common: {
displayName: "顯示名稱", // Display Name
description: "說明", // Description
submit: "送出", // Submit
cancel: "取消", // Cancel
save: "儲存", // Save
delete: "刪除", // Delete
provider: "提供者", // Provider
homePage: "首頁", // Home Page
},
titleBar: {
help: "說明", // Help
minimize: "最小化", // Minimize
maximize: "最大化", // Maximize
restore: "還原", // Restore
close: "關閉", // Close
},
homePage: {
newProject: "新專案", // New Project
openLocalProject: {
title: "打開本機專案", // Open Local Project
},
openCloudProject: {
title: "打開雲端專案", // Open Cloud Project
selectConnection: "選擇連線", // Select a Connection
},
recentProjects: "最近的專案", // Recent Projects
deleteProject: {
title: "刪除專案", // Delete Project
confirmation: "確定要刪除專案?", // Are you sure you want to delete project
},
importProject: {
title: "匯入專案", // Import Project
confirmation: "您確定要將專案${project.file.name}的設定轉換為v2格式嗎我們建議您首先備份專案文件。",
// Are you sure you want to convert project ${project.file.name} project settings to v2 format?
// We recommend you backup the project file first.
},
messages: {
deleteSuccess: "已成功刪除${project.name}專案", // Successfully deleted ${project.name}
},
},
appSettings: {
title: "應用程式設定", // Application Settings
storageTitle: "儲存空間設定", // Storage Settings
uiHelp: "您的設定存放在哪裡", // Where your settings are stored
save: "保存設定", // Save Settings
securityToken: {
name: {
title: "名稱", // Name
},
key: {
title: "鍵", // Key
},
},
securityTokens: {
title: "安全性權杖", // Security Tokens
description: "安全性權杖用於加密專案組態中的敏感資料",
// Security tokens are used to encrypt sensitive data within your project configuration
},
version: {
description: "版本:", // Version:
},
commit: "提交SHA", // Commit SHA
devTools: {
description: "打開應用程式開發工具以幫助診斷問題", // Open application developer tools to help diagnose issues
button: "切換開發工具", // Toggle Developer Tools
},
reload: {
description: "重新載入應用程式,放棄所有目前做的修改", // Reload the app discarding all current changes
button: "重新整理應用程式", // Refresh Application
},
messages: {
saveSuccess: "已成功保存應用程式設定", // Successfully saved application settings
},
},
projectSettings: {
title: "專案設定", // Project Settings
securityToken: {
title: "安全性權杖", // Security Token
description: "用於加密專案檔案中的敏感資料", // Used to encrypt sensitive data within project files
},
save: "保存專案", // Save Project
sourceConnection: {
title: "來源連線", // Source Connection
description: "從何處載入資料", // Where to load assets from
},
targetConnection: {
title: "目標連線", // Target Connection
description: "在哪裡保存專案和匯出的資料", // Where to save the project and exported data
},
videoSettings: {
title: "影片設定", // Video Settings
description: "設定影片標記的速率", // The rate at which frames are extracted for tagging.
frameExtractionRate: "影像取樣率(影像每秒的畫面數)", // Frame Extraction Rate (frames per a video second)
},
addConnection: "新增連線", // Add Connection
messages: {
saveSuccess: "已成功保存${project.name}專案設定", // Successfully saved ${project.name} project settings
},
},
projectMetrics: {
title: "專案相關指標", // Project Metrics
assetsSectionTitle: "圖像數據", // Assets
// As for this VOTT tool, translate "Assets" to "Image data" in Traditional Chinese,
// as "Asset" can be confusing if directly translated.
totalAssetCount: "圖像數據總數", // Total Assets
visitedAssets: "已檢視的圖像數據(${count}", // Visited Assets (${count})
taggedAssets: "已標記的圖像數據(${count}", // Tagged Assets (${count})
nonTaggedAssets: "未標記的圖像數據(${count}", // Not Tagged Assets (${count})
nonVisitedAssets: "未檢視的圖像數據(${count}", // Not Visited Assets (${count})
tagsSectionTitle: "標記和標籤",
// Tags & Labels, so it can actually be same translation to Tags and Labels in Traditional Chinese,
// to differentiate, having slightly different translation for both keywords.
totalRegionCount: "已標記區域總數", // Total Tagged Regions
totalTagCount: "標記總數", // Total Tags
avgTagCountPerAsset: "每個圖像數據的平均標記數", // Average tags per asset
},
tags: {
title: "標記", // Tags
placeholder: "新增標記", // Add new tag
editor: "標記編輯器", // Tags Editor
modal: {
name: "標記名稱", // Tag Name
color: "標記顏色", // Tag Color
},
colors: {
white: "白色", // White
gray: "灰色", // Gray
red: "紅色", // Red
maroon: "栗色", // Maroon
yellow: "黃色", // Yellow
olive: "橄欖", // Olive
lime: "酸橙", // Lime
green: "綠色", // Green
aqua: "水色", // Aqua
teal: "藍綠色", // Teal
blue: "藍色", // Blue
navy: "海軍", // Navy
fuschia: "紫紅色", // Fuschia
purple: "紫色", // Purple
},
warnings: {
existingName: "標記名稱已存在。請選擇其他名字", // Tag name already exists. Choose another name
emptyName: "標記名稱不能為空白", // Cannot have an empty tag name
unknownTagName: "未命名", // Unknown
},
toolbar: {
add: "新增標記", // Add new tag
search: "尋找標記", // Search tags
edit: "編輯標記", // Edit tag
lock: "鎖定標記", // Lock tag
moveUp: "向上移動標記", // Move tag up
moveDown: "向下移動標記", // Move tag down
delete: "刪除標記", // Delete tag
},
},
connections: {
title: "連線", // Connections
details: "連線細節", // Connection Details
settings: "連線設定", // Connection Settings
instructions: "請選擇一個連線進行編輯", // Please select a connection to edit
save: "保存連線", // Save Connection
messages: {
saveSuccess: "已成功儲存${connection.name}", // Successfully saved ${connection.name}
deleteSuccess: "已成功刪除${connection.name}", // Successfully deleted ${connection.name}
},
imageCorsWarning: "警告在Web瀏覽器中使用VoTT時由於CORS跨源資源共享限制來自Bing Image Search的某些圖像數據可能無法正確匯出。",
// Warning: When using VoTT in a Web browser, some assets from Bing Image Search may not export correctly
// due to CORS (Cross Origin Resource Sharing) restrictions.
blobCorsWarning: "警告必須在Azure Blob儲存體帳戶上啟用CORS跨域資源共享才能將其用作來源或目標連接。 {0}中提供了有關啟用CORS的更多資訊。",
// Warning: CORS (Cross Domain Resource Sharing) must be enabled on the Azure Blob Storage account,
// in order to use it as a source or target connection.
// More information on enabling CORS can be found in the {0}
azDocLinkText: "Azure說明文件", // Azure Documentation.
providers: {
azureBlob: {
title: "Azure Blob 儲存體", // Azure Blob Storage
description: "",
accountName: {
title: "帳戶名", // Account Name
description: "",
},
containerName: {
title: "容器名稱", // Container Name
description: "",
},
sas: {
title: "SAS", // SAS
description: "用於驗證Blob儲存體帳戶的共用存取簽章",
// Shared access signature used to authenticate to the blob storage account
},
createContainer: {
title: "新增容器", // Create Container
description: "新增blob容器如果還不存在時", // Creates the blob container if it does not already exist
},
},
bing: {
title: "Bing 影像搜尋", // Bing Image Search
options: "Bing 影像搜尋選項", // Bing Image Search Options
apiKey: "API密鑰", // API Key
query: "查詢", // Query
aspectRatio: {
title: "長寬比", // Aspect Ratio
all: "所有", // All
square: "矩形", // Square
wide: "寬", // Wide
tall: "高", // Tall
},
},
local: {
title: "本機檔案系統", // Local File System
folderPath: "資料夾路徑", // Folder Path
selectFolder: "選擇資料夾", // Select Folder
chooseFolder: "選取資料夾", // Choose Folder
},
},
},
editorPage: {
width: "寬度", // Width
height: "高度", // Height
tagged: "已標記", // Tagged
visited: "已檢視", // Visited
toolbar: {
select: "選擇 (V)", // Select (V)
pan: "全景", // Pan
drawRectangle: "繪製矩形", // Draw Rectangle
drawPolygon: "繪製多邊形", // Draw Polygon
copyRectangle: "複製矩形", // Copy Rectangle
copy: "複製區域", // Copy Regions
cut: "剪下區域", // Cut Regions
paste: "貼上區域", // Paste Regions
removeAllRegions: "刪除所有區域", // Remove All Regions
previousAsset: "以前的圖像數據", // Previous Asset
nextAsset: "下一個圖像數據", // Next Asset
saveProject: "儲存專案", // Save Project
exportProject: "匯出專案", // Export Project
activeLearning: "主動學習", // Active Learning
},
videoPlayer: {
previousTaggedFrame: {
tooltip: "上一個標記的畫面", // Previous Tagged Frame
},
nextTaggedFrame: {
tooltip: "下一個標記的畫面", // Next Tagged Frame
},
previousExpectedFrame: {
tooltip: "上一個畫面", // Previous Frame
},
nextExpectedFrame: {
tooltip: "下一個畫面", // Next Frame
},
},
help: {
title: "切換輔助說明選單", // Toggle Help Menu
escape: "離開輔助說明選單", // Escape Help Menu
},
assetError: "無法載入圖像數據", // Unable to load asset
tags: {
hotKey: {
apply: "使用快捷鍵來套用標記", // Apply Tag with Hot Key
lock: "用快捷鍵來鎖定標記", // Lock Tag with Hot Key
},
rename: {
title: "重新命名標記", // Rename Tag
confirmation: "您確定要重新命名此標記嗎?它將在所有圖像數據中被重新命名",
// Are you sure you want to rename this tag? It will be renamed throughout all assets
},
delete: {
title: "刪除標記", // Delete Tag
confirmation: "您確定要刪除此標記嗎?它將在所有圖像數據中被刪除,並且只有使用此標記的任何區域也將被刪除",
// Are you sure you want to delete this tag? It will be deleted throughout all assets
// and any regions where this is the only tag will also be deleted
},
},
canvas: {
removeAllRegions: {
title: "刪除所有區域", // Remove All Regions
confirmation: "您確定要刪除所有區域嗎?", // Are you sure you want to remove all regions?
},
},
messages: {
enforceTaggedRegions: {
title: "檢測到無效的區域", // Invalid region(s) detected
description: "一個或多個區域尚未被標記。在繼續下一個圖像數據之前,請確保所有區域均已標記。",
// 1 or more regions have not been tagged.
// Ensure all regions are tagged before continuing to next asset.
},
},
},
export: {
title: "匯出", // Export
settings: "匯出設定", // Export Settings
saveSettings: "儲存匯出設定", // Save Export Settings
providers: {
common: {
properties: {
assetState: {
title: "圖像數據狀態", // Asset State
description: "匯出項目中包括哪些圖像數據", // Which assets to include in the export
options: {
all: "所有圖像數據", // All Assets
visited: "只有已檢視的圖像數據", // Only Visited Assets
tagged: "只有已標記的圖像數據", // Only tagged Assets
},
},
testTrainSplit: {
title: "測試/訓練分割", // Test / Train Split
description: "測試訓練分割以用於匯出數據", // The test train split to use for exported data
},
includeImages: {
title: "包含圖像", // Include Images
description: "是否在目標連接中包括二進位圖像數據",
// Whether or not to include binary image assets in target connection
},
},
},
vottJson: {
displayName: "VoTT JSON", // VoTT JSON
},
azureCV: {
displayName: "Azure自訂視覺服務", // Azure Custom Vision Service
regions: {
// reference to https://azure.microsoft.com/zh-tw/global-infrastructure/geographies/
// for official translation
australiaEast: "澳大利亞東部", // Australia East
centralIndia: "印度中部", // Central India
eastUs: "美國東部", // East US
eastUs2: "美國東部 2", // East US 2
japanEast: "日本東部", // Japan East
northCentralUs: "美國中北部", // North Central US
northEurope: "北歐", // North Europe
southCentralUs: "美國中南部", // South Central US
southeastAsia: "東南亞", // Southeast Asia
ukSouth: "英國南部", // UK South
westUs2: "美國西部 2", // West US 2
westEurope: "西歐", // West Europe
},
properties: {
apiKey: {
title: "API密鑰", // API Key
},
region: {
title: "區域", // Region
description: "部署服務的Azure區域", // The Azure region where your service is deployed
},
classificationType: {
title: "分類類型", // Classification Type
options: {
multiLabel: "每個圖像多個標記", // Multiple tags per image
multiClass: "每個圖像一個標記", // Single tag per image
},
},
name: {
title: "專案名", // Project Name
},
description: {
title: "專案簡介", // Project Description
},
domainId: {
title: "領域", // Domain
},
newOrExisting: {
title: "新增專案或既有專案", // New or Existing Project
options: {
new: "新增專案", // New Project
existing: "既有專案", // Existing Project
},
},
projectId: {
title: "專案名稱", // Project Name
},
projectType: {
title: "專案類型", // Project Type
options: {
classification: "分類", // Classification
objectDetection: "物件偵測", // Object Detection
},
},
},
},
tfRecords: {
displayName: "Tensorflow記錄", // Tensorflow Records
},
pascalVoc: {
displayName: "Pascal VOC", // Pascal VOC
exportUnassigned: {
title: "匯出未指定的項目", // Export Unassigned
description: "是否在已匯出的數據中包括未指定的標記", // Whether or not to include unassigned tags in exported data
},
},
cntk: {
displayName: "Microsoft Cognitive ToolkitCNTK)", // Microsoft Cognitive Toolkit (CNTK)
},
csv: {
displayName: "逗號分隔格式 (CSV)", // Comma Separated Values (CSV)
},
},
messages: {
saveSuccess: "已成功儲存匯出設定", // Successfully saved export settings
},
},
activeLearning: {
title: "主動學習", // Active Learning
form: {
properties: {
modelPathType: {
title: "模型提供者", // Model Provider
description: "從何處載入訓練模型", // Where to load the training model from
options: {
preTrained: "預先訓練Coco SSD", // Pre-trained Coco SSD
customFilePath: "自訂(檔案路徑)", // Custom (File path)
customWebUrl: "自訂 (URL)", // Custom (Url)
},
},
autoDetect: {
title: "自動偵測", // Auto Detect
description: "在圖像數據之間瀏覽時是否自動進行預測",
// Whether or not to automatically make predictions as you navigate between assets
},
modelPath: {
title: "模型路徑", // Model path
description: "從本機檔案系統中選擇模型", // Select a model from your local file system
},
modelUrl: {
title: "模型網址", // Model URL
description: "從公共網址載入模型", // Load your model from a public web URL
},
predictTag: {
title: "預測標記", // Predict Tag
description: "是否在預測中自動包含標記", // Whether or not to automatically include tags in predictions
},
},
},
messages: {
loadingModel: "正在載入主動學習模型...", // Loading active learning model...
errorLoadModel: "載入主動學習模型時出現錯誤", // Error loading active learning model
saveSuccess: "已成功儲存主動學習設定", // Successfully saved active learning settings
},
},
profile: {
settings: "個人資料設定", // Profile Settings
},
errors: {
unknown: {
title: "未知錯誤", // Unknown Error
message: "該應用程式遇到未知錯誤。請再試一遍。", // The app encountered an unknown error. Please try again.
},
projectUploadError: {
title: "上傳檔案時出現錯誤", // Error Uploading File
message: "上傳檔案時出現錯誤。請確認檔案格式正確,然後重試。",
// There was an error uploading the file. Please verify the file is of the correct format and try again.
},
genericRenderError: {
title: "載入應用程序時出現錯誤", // Error Loading Application
message: "轉譯應用程序時發生錯誤。請再試一遍", // An error occured while rendering the application. Please try again
},
projectInvalidSecurityToken: {
title: "載入專案文件時出現錯誤", // Error loading project file
message: "專案使用的安全性認證無效。請驗證是否在您的應用程式設定中正確的設定了專案的安全性認證",
// The security token referenced by the project is invalid. Verify that the security token
// for the project has been set correctly within your application settings
},
projectInvalidJson: {
title: "解析專案文件時出現錯誤", // Error parsing project file
message: "所選擇的專案文件不包含有效的JSON格式。請確認該專案檔案並且重試。",
// The selected project files does not contain valid JSON. Please check the file any try again.
},
projectDeleteError: {
title: "刪除專案時出現錯誤", // Error deleting project
message: "刪除專案時發生錯誤。請確認專案檔案和安全性認證是否存在,然後重試",
// An error occured while deleting the project.
// Validate the project file and security token exist and try again
},
securityTokenNotFound: {
title: "載入專案檔案時出現錯誤", // Error loading project file
message: "在當前的應用程式設定中找不到該專案所使用的安全性認證。請確認安全性認證是否存在,然後嘗試重新載入專案。",
// The security token referenced by the project cannot be found in your current application settings.
// Verify the security token exists and try to reload the project.
},
canvasError: {
title: "載入畫面時出現錯誤", // Error loading canvas
message: "載入畫面時發生錯誤,請檢查專案的圖像數據,然後重試。",
// There was an error loading the canvas, check the project's assets and try again.
},
importError: {
title: "匯入V1格式專案時出現錯誤", // Error importing V1 project
message: "匯入V1格式專案時出現錯誤。請檢查專案檔案然後重試",
// There was an error importing the V1 project. Check the project file and try again
},
pasteRegionTooBigError: {
title: "貼上區域時發生錯誤", // Error pasting region
message: "此區域對於這一個圖像數據太大了。請嘗試複製其他的區域",
// Region too big for this asset. Try copying another region
},
exportFormatNotFound: {
title: "匯出專案時出現錯誤", // Error exporting project
message: "專案設定中缺少匯出格式。請在匯出設定畫面中選擇一種匯出格式。",
// Project is missing export format. Please select an export format in the export setting page.
},
activeLearningPredictionError: {
title: "主動學習錯誤", // Active Learning Error
message: "在預測當前圖像數據中的區域時發生錯誤。請確認您的主動學習相關設定,然後重試",
// An error occurred while predicting regions in the current asset.
// Please verify your active learning configuration and try again
},
},
};

Просмотреть файл

@ -1,8 +1,19 @@
import { strings, addLocValues, IAppStrings, interpolate, interpolateJson } from "./strings";
import { english } from "./localization/en-us";
import { spanish } from "./localization/es-cl";
import { japanese } from "./localization/ja";
import { chinesetw } from "./localization/zh-tw";
import { korean } from "./localization/ko-kr";
import { chinese } from "./localization/zh-ch";
const languages = ["en", "es"];
const languages = [
"en",
"es",
"ja",
"tw",
"ko",
"ch",
];
describe("Localization tests", () => {
@ -10,6 +21,10 @@ describe("Localization tests", () => {
return {
en: english,
es: spanish,
ja: japanese,
tw: chinesetw,
ko: korean,
ch: chinese,
}[language];
}

Просмотреть файл

@ -1,6 +1,10 @@
import LocalizedStrings, { LocalizedStringsMethods } from "react-localization";
import { english } from "./localization/en-us";
import { spanish } from "./localization/es-cl";
import { japanese } from "./localization/ja";
import { chinesetw } from "./localization/zh-tw";
import { korean } from "./localization/ko-kr";
import { chinese } from "./localization/zh-ch";
/**
* Interface for all required strings in application
@ -452,8 +456,13 @@ interface IErrorMetadata {
interface IStrings extends LocalizedStringsMethods, IAppStrings { }
export const strings: IStrings = new LocalizedStrings({
// TODO: Need to comment out other languages which will not be used
en: english,
es: spanish,
ja: japanese,
tw: chinesetw,
ko: korean,
ch: chinese,
});
/**

Просмотреть файл

@ -22,12 +22,10 @@ input[type=file] {
box-shadow: inset 0 0 6px $darker-3;
background-color: $lighter-1;
border-radius: 10px;
border-radius: 10px;
}
/* Handle */
::-webkit-scrollbar-thumb {
border-radius: 10px;
border-radius: 10px;
background: $lighter-4;
box-shadow: 0 0 6px $darker-3;

Просмотреть файл

@ -115,9 +115,37 @@ describe("CNTK Export Provider", () => {
const assetsToExport = await getAssetsSpy.mock.results[0].value;
const testSplit = (100 - (defaultOptions.testTrainSplit || 80)) / 100;
const testCount = Math.ceil(assetsToExport.length * testSplit);
const testArray = assetsToExport.slice(0, testCount);
const trainArray = assetsToExport.slice(testCount, assetsToExport.length);
const trainArray = [];
const testArray = [];
const tagsAssestList: {
[index: string]: {
assetSet: Set<string>,
testArray: string[],
trainArray: string[],
},
} = {};
testProject.tags.forEach((tag) =>
tagsAssestList[tag.name] = {
assetSet: new Set(), testArray: [],
trainArray: [],
});
assetsToExport.forEach((assetMetadata) => {
assetMetadata.regions.forEach((region) => {
region.tags.forEach((tagName) => {
if (tagsAssestList[tagName]) {
tagsAssestList[tagName].assetSet.add(assetMetadata.asset.name);
}
});
});
});
for (const tagKey of Object.keys(tagsAssestList)) {
const assetSet = tagsAssestList[tagKey].assetSet;
const testCount = Math.ceil(assetSet.size * testSplit);
testArray.push(...Array.from(assetSet).slice(0, testCount));
trainArray.push(...Array.from(assetSet).slice(testCount, assetSet.size));
}
const storageProviderMock = LocalFileSystemProxy as any;
const writeBinaryCalls = storageProviderMock.mock.instances[0].writeBinary.mock.calls;

Просмотреть файл

@ -3,6 +3,7 @@ import { ExportProvider, IExportResults } from "./exportProvider";
import { IAssetMetadata, IExportProviderOptions, IProject } from "../../models/applicationState";
import HtmlFileReader from "../../common/htmlFileReader";
import Guard from "../../common/guard";
import { splitTestAsset } from "./testAssetsSplitHelper";
enum ExportSplit {
Test,
@ -33,13 +34,17 @@ export class CntkExportProvider extends ExportProvider<ICntkExportProviderOption
public async export(): Promise<IExportResults> {
await this.createFolderStructure();
const assetsToExport = await this.getAssetsForExport();
const testAssets: string[] = [];
const testSplit = (100 - (this.options.testTrainSplit || 80)) / 100;
const testCount = Math.ceil(assetsToExport.length * testSplit);
const testArray = assetsToExport.slice(0, testCount);
if (testSplit > 0 && testSplit <= 1) {
const splittedAssets = splitTestAsset(assetsToExport, this.project.tags, testSplit);
testAssets.push(...splittedAssets);
}
const results = await assetsToExport.mapAsync(async (assetMetadata) => {
try {
const exportSplit = testArray.find((am) => am.asset.id === assetMetadata.asset.id)
const exportSplit = testAssets.find((am) => am === assetMetadata.asset.id)
? ExportSplit.Test
: ExportSplit.Train;

Просмотреть файл

@ -69,7 +69,9 @@ describe("PascalVOC Json Export Provider", () => {
beforeEach(() => {
const assetServiceMock = AssetService as jest.Mocked<typeof AssetService>;
assetServiceMock.prototype.getAssetMetadata = jest.fn((asset) => {
const mockTag = MockFactory.createTestTag();
const mockTag1 = MockFactory.createTestTag("1");
const mockTag2 = MockFactory.createTestTag("2");
const mockTag = Number(asset.id.split("-")[1]) > 7 ? mockTag1 : mockTag2;
const mockRegion1 = MockFactory.createTestRegion("region-1", [mockTag.name]);
const mockRegion2 = MockFactory.createTestRegion("region-2", [mockTag.name]);
@ -352,27 +354,70 @@ describe("PascalVOC Json Export Provider", () => {
};
const testProject = { ...baseTestProject };
const testAssets = MockFactory.createTestAssets(10, 0);
const testAssets = MockFactory.createTestAssets(13, 0);
testAssets.forEach((asset) => asset.state = AssetState.Tagged);
testProject.assets = _.keyBy(testAssets, (asset) => asset.id);
testProject.tags = [MockFactory.createTestTag("1")];
testProject.tags = MockFactory.createTestTags(3);
const exportProvider = new PascalVOCExportProvider(testProject, options);
const getAssetsSpy = jest.spyOn(exportProvider, "getAssetsForExport");
await exportProvider.export();
const storageProviderMock = LocalFileSystemProxy as any;
const writeTextFileCalls = storageProviderMock.mock.instances[0].writeText.mock.calls as any[];
const valDataIndex = writeTextFileCalls
const valDataIndex1 = writeTextFileCalls
.findIndex((args) => args[0].endsWith("/ImageSets/Main/Tag 1_val.txt"));
const trainDataIndex = writeTextFileCalls
const trainDataIndex1 = writeTextFileCalls
.findIndex((args) => args[0].endsWith("/ImageSets/Main/Tag 1_train.txt"));
const valDataIndex2 = writeTextFileCalls
.findIndex((args) => args[0].endsWith("/ImageSets/Main/Tag 2_val.txt"));
const trainDataIndex2 = writeTextFileCalls
.findIndex((args) => args[0].endsWith("/ImageSets/Main/Tag 2_train.txt"));
const expectedTrainCount = (testTrainSplit / 100) * testAssets.length;
const expectedTestCount = ((100 - testTrainSplit) / 100) * testAssets.length;
const assetsToExport = await getAssetsSpy.mock.results[0].value;
const trainArray = [];
const testArray = [];
const tagsAssestList: {
[index: string]: {
assetSet: Set<string>,
testArray: string[],
trainArray: string[],
},
} = {};
testProject.tags.forEach((tag) =>
tagsAssestList[tag.name] = {
assetSet: new Set(), testArray: [],
trainArray: [],
});
assetsToExport.forEach((assetMetadata) => {
assetMetadata.regions.forEach((region) => {
region.tags.forEach((tagName) => {
if (tagsAssestList[tagName]) {
tagsAssestList[tagName].assetSet.add(assetMetadata.asset.name);
}
});
});
});
expect(writeTextFileCalls[valDataIndex][1].split("\n")).toHaveLength(expectedTestCount);
expect(writeTextFileCalls[trainDataIndex][1].split("\n")).toHaveLength(expectedTrainCount);
for (const tagKey of Object.keys(tagsAssestList)) {
const assetSet = tagsAssestList[tagKey].assetSet;
const testCount = Math.ceil(((100 - testTrainSplit) / 100) * assetSet.size);
tagsAssestList[tagKey].testArray = Array.from(assetSet).slice(0, testCount);
tagsAssestList[tagKey].trainArray = Array.from(assetSet).slice(testCount, assetSet.size);
testArray.push(...tagsAssestList[tagKey].testArray);
trainArray.push(...tagsAssestList[tagKey].trainArray);
}
expect(writeTextFileCalls[valDataIndex1][1].split(/\r?\n/).filter((line) =>
line.endsWith(" 1"))).toHaveLength(tagsAssestList["Tag 1"].testArray.length);
expect(writeTextFileCalls[trainDataIndex1][1].split(/\r?\n/).filter((line) =>
line.endsWith(" 1"))).toHaveLength(tagsAssestList["Tag 1"].trainArray.length);
expect(writeTextFileCalls[valDataIndex2][1].split(/\r?\n/).filter((line) =>
line.endsWith(" 1"))).toHaveLength(tagsAssestList["Tag 2"].testArray.length);
expect(writeTextFileCalls[trainDataIndex2][1].split(/\r?\n/).filter((line) =>
line.endsWith(" 1"))).toHaveLength(tagsAssestList["Tag 2"].trainArray.length);
}
it("Correctly generated files based on 50/50 test / train split", async () => {

Просмотреть файл

@ -6,6 +6,7 @@ import HtmlFileReader from "../../common/htmlFileReader";
import { itemTemplate, annotationTemplate, objectTemplate } from "./pascalVOC/pascalVOCTemplates";
import { interpolate } from "../../common/strings";
import os from "os";
import { splitTestAsset } from "./testAssetsSplitHelper";
interface IObjectInfo {
name: string;
@ -253,40 +254,58 @@ export class PascalVOCExportProvider extends ExportProvider<IPascalVOCExportProv
}
});
// Save ImageSets
await tags.forEachAsync(async (tag) => {
const tagInstances = tagUsage.get(tag.name) || 0;
if (!exportUnassignedTags && tagInstances === 0) {
return;
}
if (testSplit > 0 && testSplit <= 1) {
const tags = this.project.tags;
const testAssets: string[] = splitTestAsset(allAssets, tags, testSplit);
const assetList = [];
assetUsage.forEach((tags, assetName) => {
if (tags.has(tag.name)) {
assetList.push(`${assetName} 1`);
} else {
assetList.push(`${assetName} -1`);
await tags.forEachAsync(async (tag) => {
const tagInstances = tagUsage.get(tag.name) || 0;
if (!exportUnassignedTags && tagInstances === 0) {
return;
}
});
if (testSplit > 0 && testSplit <= 1) {
// Split in Test and Train sets
const totalAssets = assetUsage.size;
const testCount = Math.ceil(totalAssets * testSplit);
const testArray = assetList.slice(0, testCount);
const trainArray = assetList.slice(testCount, totalAssets);
const testArray = [];
const trainArray = [];
assetUsage.forEach((tags, assetName) => {
let assetString = "";
if (tags.has(tag.name)) {
assetString = `${assetName} 1`;
} else {
assetString = `${assetName} -1`;
}
if (testAssets.find((am) => am === assetName)) {
testArray.push(assetString);
} else {
trainArray.push(assetString);
}
});
const testImageSetFileName = `${imageSetsMainFolderName}/${tag.name}_val.txt`;
await this.storageProvider.writeText(testImageSetFileName, testArray.join(os.EOL));
const trainImageSetFileName = `${imageSetsMainFolderName}/${tag.name}_train.txt`;
await this.storageProvider.writeText(trainImageSetFileName, trainArray.join(os.EOL));
});
} else {
// Save ImageSets
await tags.forEachAsync(async (tag) => {
const tagInstances = tagUsage.get(tag.name) || 0;
if (!exportUnassignedTags && tagInstances === 0) {
return;
}
const assetList = [];
assetUsage.forEach((tags, assetName) => {
if (tags.has(tag.name)) {
assetList.push(`${assetName} 1`);
} else {
assetList.push(`${assetName} -1`);
}
});
} else {
const imageSetFileName = `${imageSetsMainFolderName}/${tag.name}.txt`;
await this.storageProvider.writeText(imageSetFileName, assetList.join(os.EOL));
}
});
});
}
}
}

Просмотреть файл

@ -0,0 +1,61 @@
import _ from "lodash";
import {
IAssetMetadata, AssetState, IRegion,
RegionType, IPoint, IExportProviderOptions,
} from "../../models/applicationState";
import MockFactory from "../../common/mockFactory";
import { splitTestAsset } from "./testAssetsSplitHelper";
import { appInfo } from "../../common/appInfo";
describe("splitTestAsset Helper tests", () => {
describe("Test Train Splits", () => {
async function testTestTrainSplit(testTrainSplit: number): Promise<void> {
const assetArray = MockFactory.createTestAssets(13, 0);
const tags = MockFactory.createTestTags(2);
assetArray.forEach((asset) => asset.state = AssetState.Tagged);
const testSplit = (100 - testTrainSplit) / 100;
const testCount = Math.ceil(testSplit * assetArray.length);
const assetMetadatas = assetArray.map((asset, i) =>
MockFactory.createTestAssetMetadata(asset,
i < (assetArray.length - testCount) ?
[MockFactory.createTestRegion("Region" + i, [tags[0].name])] :
[MockFactory.createTestRegion("Region" + i, [tags[1].name])]));
const testAssetsNames = splitTestAsset(assetMetadatas, tags, testSplit);
const trainAssetsArray = assetMetadatas.filter((assetMetadata) =>
testAssetsNames.indexOf(assetMetadata.asset.name) < 0);
const testAssetsArray = assetMetadatas.filter((assetMetadata) =>
testAssetsNames.indexOf(assetMetadata.asset.name) >= 0);
const expectedTestCount = Math.ceil(testSplit * testCount) +
Math.ceil(testSplit * (assetArray.length - testCount));
expect(testAssetsNames).toHaveLength(expectedTestCount);
expect(trainAssetsArray.length + testAssetsArray.length).toEqual(assetMetadatas.length);
expect(testAssetsArray).toHaveLength(expectedTestCount);
expect(testAssetsArray.filter((assetMetadata) => assetMetadata.regions[0].tags[0] === tags[0].name).length)
.toBeGreaterThan(0);
expect(testAssetsArray.filter((assetMetadata) => assetMetadata.regions[0].tags[0] === tags[1].name).length)
.toBeGreaterThan(0);
}
it("Correctly generated files based on 50/50 test / train split", async () => {
await testTestTrainSplit(50);
});
it("Correctly generated files based on 60/40 test / train split", async () => {
await testTestTrainSplit(60);
});
it("Correctly generated files based on 80/20 test / train split", async () => {
await testTestTrainSplit(80);
});
it("Correctly generated files based on 90/10 test / train split", async () => {
await testTestTrainSplit(90);
});
});
});

Просмотреть файл

@ -0,0 +1,30 @@
import { IAssetMetadata, ITag } from "../../models/applicationState";
/**
* A helper function to split train and test assets
* @param template String containing variables
* @param params Params containing substitution values
*/
export function splitTestAsset(allAssets: IAssetMetadata[], tags: ITag[], testSplitRatio: number): string[] {
if (testSplitRatio <= 0 || testSplitRatio > 1) { return []; }
const testAssets: string[] = [];
const tagsAssetDict: { [index: string]: { assetList: Set<string> } } = {};
tags.forEach((tag) => tagsAssetDict[tag.name] = { assetList: new Set() });
allAssets.forEach((assetMetadata) => {
assetMetadata.regions.forEach((region) => {
region.tags.forEach((tagName) => {
if (tagsAssetDict[tagName]) {
tagsAssetDict[tagName].assetList.add(assetMetadata.asset.name);
}
});
});
});
for (const tagKey of Object.keys(tagsAssetDict)) {
const assetList = tagsAssetDict[tagKey].assetList;
const testCount = Math.ceil(assetList.size * testSplitRatio);
testAssets.push(...Array.from(assetList).slice(0, testCount));
}
return testAssets;
}

Просмотреть файл

@ -58,7 +58,7 @@ export class ConnectionPicker extends React.Component<IConnectionPickerProps, IC
<option
className="connection-option"
key={connection.id}
value={connection.id}>{connection.name}
value={connection.id}>{this.getConnectionText(connection)}
</option>)
}
</select>
@ -71,6 +71,18 @@ export class ConnectionPicker extends React.Component<IConnectionPickerProps, IC
);
}
private getConnectionText = (connection: IConnection): string => {
const options = connection.providerOptions;
if (options["folderPath"]) {
return `${connection.name} (${options["folderPath"]})`;
} else if (options["accountName"]) {
return `${connection.name} (Azure:${options["accountName"]}\\${options["containerName"]})`;
} else {
return connection.name;
}
}
private onChange = (e) => {
const selectedConnection = this.props.connections
.find((connection) => connection.id === e.target.value) || {};

Просмотреть файл

@ -42,7 +42,7 @@ export default class EditorSideBar extends React.Component<IEditorSideBarProps,
public render() {
return (
<div className="editor-page-sidebar-nav">
<div className="editor-page-bottombar-nav">
<AutoSizer>
{({ height, width }) => (
<List

Просмотреть файл

@ -4,4 +4,12 @@ import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
// Silence console.log and console.group statements in testing
console.log = console.group = function() {};
console.log = console.group = function() {};
const electronMock = {
ipcRenderer: {
send: jest.fn(),
on: jest.fn(),
},
};
window.require = jest.fn(() => electronMock);