diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a8b07aa07d09..33ed57dc5629 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -24,8 +24,8 @@ /sdk/batch/ @xingwu1 @matthchr @bgklein /sdk/cosmosdb/ @southpolesteve -/sdk/eventhub/ @ramya-rao-a @chradek @richardpark-msft -/sdk/servicebus/ @ramya-rao-a @chradek @richardpark-msft @HarshaNalluru +/sdk/eventhub/ @ramya-rao-a @chradek @richardpark-msft +/sdk/servicebus/ @ramya-rao-a @chradek @richardpark-msft @HarshaNalluru /sdk/identity/ @schaabs @daviwil @jonathandturner /sdk/keyvault/ @jonathandturner @sadasant @@ -33,6 +33,7 @@ /sdk/test-utils/ @HarshaNalluru @sadasant /sdk/textanalytics/ @xirzec @willmtemple +/sdk/formrecognizer/ @jeremymeng @willmtemple /sdk/search/ @xirzec diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 998eda75ff00..d255871b5658 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1,5 +1,6 @@ dependencies: '@rush-temp/abort-controller': 'file:projects/abort-controller.tgz' + '@rush-temp/ai-form-recognizer': 'file:projects/ai-form-recognizer.tgz' '@rush-temp/ai-text-analytics': 'file:projects/ai-text-analytics.tgz' '@rush-temp/app-configuration': 'file:projects/app-configuration.tgz' '@rush-temp/core-amqp': 'file:projects/core-amqp.tgz' @@ -7281,9 +7282,65 @@ packages: dev: false name: '@rush-temp/abort-controller' resolution: - integrity: sha512-9koE25abx6yJKJWqFFfF+qIV5rEszmj0lx5GZwclzpn5JnKbazRRdgFsmQRKh1L8hVf8HWs0uYsmOSE6Awd3Wg== + integrity: sha512-k51GFnXbP2QALN4HIGLG6Etw2CqrWxSmJgbzGqmOxzRo37icWzainwZFKdi1tt8/XRkDcKhi7EMOa/sUHk8Gmg== tarball: 'file:projects/abort-controller.tgz' version: 0.0.0 + 'file:projects/ai-form-recognizer.tgz': + dependencies: + '@azure/core-tracing': 1.0.0-preview.7 + '@microsoft/api-extractor': 7.7.12 + '@opentelemetry/types': 0.2.0 + '@rollup/plugin-commonjs': 11.0.2_rollup@1.32.1 + '@rollup/plugin-json': 4.0.2_rollup@1.32.1 + '@rollup/plugin-multi-entry': 3.0.0_rollup@1.32.1 + '@rollup/plugin-node-resolve': 7.1.1_rollup@1.32.1 + '@rollup/plugin-replace': 2.3.1_rollup@1.32.1 + '@types/chai': 4.2.11 + '@types/mocha': 7.0.2 + '@types/node': 8.10.59 + '@types/sinon': 7.5.2 + '@typescript-eslint/eslint-plugin': 2.27.0_7cc55bcb3036489d096f3a4764bec0b9 + '@typescript-eslint/parser': 2.27.0_eslint@6.8.0+typescript@3.7.5 + chai: 4.2.0 + chai-as-promised: 7.1.1_chai@4.2.0 + cross-env: 6.0.3 + dotenv: 8.2.0 + eslint: 6.8.0 + eslint-config-prettier: 6.10.1_eslint@6.8.0 + eslint-plugin-no-null: 1.0.2_eslint@6.8.0 + eslint-plugin-no-only-tests: 2.4.0 + eslint-plugin-promise: 4.2.1 + karma: 4.4.1 + karma-chrome-launcher: 3.1.0 + karma-coverage: 2.0.1 + karma-edge-launcher: 0.4.2_karma@4.4.1 + karma-env-preprocessor: 0.1.1 + karma-firefox-launcher: 1.3.0 + karma-ie-launcher: 1.0.0_karma@4.4.1 + karma-json-preprocessor: 0.3.3_karma@4.4.1 + karma-json-to-file-reporter: 1.0.1 + karma-junit-reporter: 2.0.1_karma@4.4.1 + karma-mocha: 1.3.0 + karma-mocha-reporter: 2.2.5_karma@4.4.1 + karma-remap-istanbul: 0.6.0_karma@4.4.1 + mocha: 7.1.1 + mocha-junit-reporter: 1.23.3_mocha@7.1.1 + prettier: 1.19.1 + rimraf: 3.0.2 + rollup: 1.32.1 + rollup-plugin-sourcemaps: 0.4.2_rollup@1.32.1 + rollup-plugin-terser: 5.3.0_rollup@1.32.1 + rollup-plugin-visualizer: 3.3.2_rollup@1.32.1 + sinon: 7.5.0 + source-map-support: 0.5.16 + tslib: 1.11.1 + typescript: 3.7.5 + dev: false + name: '@rush-temp/ai-form-recognizer' + resolution: + integrity: sha512-ltkm/0rAWZNddvzuOwtWFcS8l2pnQj7/BWDbmpkKL+fpvaUTW2gVUk4rEp5NODAFkI52S84vHhxrI/N4NvgvzA== + tarball: 'file:projects/ai-form-recognizer.tgz' + version: 0.0.0 'file:projects/ai-text-analytics.tgz': dependencies: '@azure/core-tracing': 1.0.0-preview.7 @@ -7340,7 +7397,7 @@ packages: dev: false name: '@rush-temp/ai-text-analytics' resolution: - integrity: sha512-vA9npAdVK1p07JMi1zv7Tzl7fdhb1uDjJBnnPNGYqOT15LTSoTCYLUeBUW8E/RzWWPWzOEvFA1M/rTwYEpzKlw== + integrity: sha512-2/pPY5KElkBxkXOHHi9bklgCUdUj5pLu/+hf0nRqf/FE8TDBSEE9yM64jodgCwpjxx/SdqADW3yH+7+vfMxhBg== tarball: 'file:projects/ai-text-analytics.tgz' version: 0.0.0 'file:projects/app-configuration.tgz': @@ -7381,7 +7438,7 @@ packages: dev: false name: '@rush-temp/app-configuration' resolution: - integrity: sha512-5I/u3ut6w3B7rUNi3t5JT0STWGIndC8QihZ7TxwZ8KFgDVbJlvsFeofO/wQh+4jtGVNkbFyU9r1VOPpXjlJNBA== + integrity: sha512-RwPNjWINsgkjOCTuXgja68tlEDntgftt4jnrdcbe5ekc78Am5JSELWjLCY6vg2IPfrmWJk+mcmh0jyQez2ShTA== tarball: 'file:projects/app-configuration.tgz' version: 0.0.0 'file:projects/core-amqp.tgz': @@ -7447,7 +7504,7 @@ packages: dev: false name: '@rush-temp/core-amqp' resolution: - integrity: sha512-ve1wAGhjcTVjRwXM6s8m+Xa6XaY4Fma2LlczOBL3cVjV7oFEvh3Xar84gFe4Ybzucp3gS/llQ24FHzyqWxnOmA== + integrity: sha512-vu7fs4tahPwtnFSvnFnWmbTe1mo/8hIEZOt1xxXLqFL846XZTC6j2IZhVkCLM1w/fmQ6XhSdp7ZHQL4xw8Tzgw== tarball: 'file:projects/core-amqp.tgz' version: 0.0.0 'file:projects/core-arm.tgz': @@ -7480,7 +7537,7 @@ packages: dev: false name: '@rush-temp/core-arm' resolution: - integrity: sha512-V0jMLzLtzMnNmyI0Cd636RcwrhPoYnrZBbhCG1xiTxxqW8WkKIP8/URbV6ZDOMic59eeNZvSRf5t7bMb0yPwkA== + integrity: sha512-gFv/kH/xYzVWwBNCXxT53WQZdwi9lR37+cRDus1WJTsxynyCg78Hh5cJOKfR3puEekdfbZigE8W1bZeZNCUyqQ== tarball: 'file:projects/core-arm.tgz' version: 0.0.0 'file:projects/core-asynciterator-polyfill.tgz': @@ -7498,7 +7555,7 @@ packages: dev: false name: '@rush-temp/core-asynciterator-polyfill' resolution: - integrity: sha512-jDLggg/4sSiSb9xXQUsivEzh+w65cXOMPe/kPArXFs+LmAHaK1Et7soO3hxgmVW7AZ/5lVp8jcfW3hDXbKIFXQ== + integrity: sha512-/hL/drdCIk4D5zjI44zWW1xuoCMjbj+bJ2sNet2d9JzWFinZfZdpw2m0kBeuhvFd7wdQNrjmxgm5tsSp/1WZpw== tarball: 'file:projects/core-asynciterator-polyfill.tgz' version: 0.0.0 'file:projects/core-auth.tgz': @@ -7539,7 +7596,7 @@ packages: dev: false name: '@rush-temp/core-auth' resolution: - integrity: sha512-P52y2+pBY+cPzrvKhuXL5aN8odhhCpk8b7/bt94BQiLWyqGkgeI4uCJSgK6kNRazE9zRBgXLPy91n+UsuFH6yQ== + integrity: sha512-UKoasmbhpWYjnC6AWIIVdukHj/H2bPHSKoVrjZXQHHksOoRGKvEnI00ESEwRiivJnFoM4HwylT8ioFi8CwiwqQ== tarball: 'file:projects/core-auth.tgz' version: 0.0.0 'file:projects/core-http.tgz': @@ -7615,7 +7672,7 @@ packages: dev: false name: '@rush-temp/core-http' resolution: - integrity: sha512-hyQj/CXDUP8OynSDrr0s4D3JAFfskTgQ12TE4t+a0f7Nrkv3UKlYEeqqSTANJUv2sTr9HzsWYAOY82jvhYdJ0A== + integrity: sha512-RU/FADKv3G1meLUEEg00d81vLv5gpRpUcpw5KffRV/65uLsI5vORfXK/pFpuYJX9UbpIT2fdZothO2u1UWTUaA== tarball: 'file:projects/core-http.tgz' version: 0.0.0 'file:projects/core-lro.tgz': @@ -7670,7 +7727,7 @@ packages: dev: false name: '@rush-temp/core-lro' resolution: - integrity: sha512-idFBNH1x3cUdY35gQWBNf7k3B6/yoAxa25Mmf32/yyLt4H99D9bExv7pmBRnS/W+RVSx2GLGbEMdB+xcDRnB3Q== + integrity: sha512-Zgvf06SPViVc3egsVICG2jw4TyjO2GOQ3Kw4nvTvnLlpt2WGZZR81ytmwMJ/ln1AHOTVsENHQCMHscSeRhCG6g== tarball: 'file:projects/core-lro.tgz' version: 0.0.0 'file:projects/core-paging.tgz': @@ -7688,7 +7745,7 @@ packages: dev: false name: '@rush-temp/core-paging' resolution: - integrity: sha512-rRAaeqAsySfmVMMf46j6TljIPw2xVRZBVuyU9wrvQKJb1VzIEBZayG1jm9iTyyqUdFOEMn759//IfYP7KkhJNQ== + integrity: sha512-taV+fDTQ3mRlWJrUObmC/AvvkfyTsG1q3FMLPx+YHVGt9ngPOxgMD5Ob5Tl/R3Hkszo+KpvHWlPGIazkKDY5SA== tarball: 'file:projects/core-paging.tgz' version: 0.0.0 'file:projects/core-tracing.tgz': @@ -7728,7 +7785,7 @@ packages: dev: false name: '@rush-temp/core-tracing' resolution: - integrity: sha512-zhQkImIaL4rpxda0p0sQVbM2GhqaTy/0pLbidw/QTRS8+a+aLJXoOoWOG6lqrvvKxsaZr+jEo7jpv176bAy+jA== + integrity: sha512-WJgpKFzLyqpLF/rTHHciy7gIsmqvnXuEqbJyM66kmyA2PAGNuIBQ9e/ZIR882M2smqEGIpUQKGSiO9Yec/B/NQ== tarball: 'file:projects/core-tracing.tgz' version: 0.0.0 'file:projects/cosmos.tgz': @@ -7789,7 +7846,7 @@ packages: dev: false name: '@rush-temp/cosmos' resolution: - integrity: sha512-8T36/iPi8KxIyL4z/Imfw43ze61JKwATJjBz8Lxmydcp/ixnLnz+zFmPJAf1K8UEbr44OYjjYPoIJJ5Qub3cVA== + integrity: sha512-NcXgn2wVel26BfpACujJH/UIK96S0s148gXDk4190Qfvik6oDQMP+nlJ6hYKApf5BRv9K1uwRKTVX7kzg77vxw== tarball: 'file:projects/cosmos.tgz' version: 0.0.0 'file:projects/eslint-plugin-azure-sdk.tgz': @@ -7822,7 +7879,7 @@ packages: dev: false name: '@rush-temp/eslint-plugin-azure-sdk' resolution: - integrity: sha512-ZxedM0WTtdZDhBZ7WcjQon13C9kNvVfFk19Hz2P+cQfBj05X1LQkDiswtvP0aqCOXljBPMX/LnJPDEV+d9gNMA== + integrity: sha512-YDFfJKyJTTh70tZTy7DFbjwoJfef96oZAFB4jPdFiJsDQqvmdoWMP2ceWuXii0+H51Tq4Vex+Nw71Ly9pBqwxw== tarball: 'file:projects/eslint-plugin-azure-sdk.tgz' version: 0.0.0 'file:projects/event-hubs.tgz': @@ -7900,7 +7957,7 @@ packages: dev: false name: '@rush-temp/event-hubs' resolution: - integrity: sha512-LMWZRpDyPa4oAqntljchX6MN08OXZ375kfl9G8yEMUPLmyP/O+p66UscBB5lvrDj5rO00++XDF7tEymRNLi9ag== + integrity: sha512-11dG95a3SI56rYv8yEfbXz+W8phQoOnz7NvybIrC+iChoQcfX0+iriEeOCMBB1/bx6B9imw0TYDL+1o8sND5dQ== tarball: 'file:projects/event-hubs.tgz' version: 0.0.0 'file:projects/event-processor-host.tgz': @@ -7957,7 +8014,7 @@ packages: dev: false name: '@rush-temp/event-processor-host' resolution: - integrity: sha512-fNCwRjda2uAvVktN/ucftH1CJRvE16bn7eg1UTdYz+oPfUcQCnEzM510vYad9e8voaWzeGILQyoLCY4v+1E3bQ== + integrity: sha512-W8yYMWh7n6/HCPgRykqZUXzfHngQAsbHFzSZHf+IEOi/cNO9Ctz8dZeX/CBMLpNetDJIjrZ9tt5aRzp3CqbI+A== tarball: 'file:projects/event-processor-host.tgz' version: 0.0.0 'file:projects/eventhubs-checkpointstore-blob.tgz': @@ -8021,7 +8078,7 @@ packages: dev: false name: '@rush-temp/eventhubs-checkpointstore-blob' resolution: - integrity: sha512-366aijZTzfitoIbpqSz19k49g7aDtmqpq2i5N6Gd0aecHipNQcLRP2kmAg6QK4aHIlPFMjWxMMkuL9o/c1W42A== + integrity: sha512-OjfXsNJfZPzHDQqoofeYZ4aIemh35jORxSRwr63Q3Z3QZ+yBhUEDuEwUyhK1DDOX1QUq6G2civGjxuExNN1ceQ== tarball: 'file:projects/eventhubs-checkpointstore-blob.tgz' version: 0.0.0 'file:projects/identity.tgz': @@ -8077,7 +8134,7 @@ packages: dev: false name: '@rush-temp/identity' resolution: - integrity: sha512-oeoyEjhK/XLfVGLZvOcvDwQ9lor6863HqabLiP0KoULTRwkcZRLGQigL5ROQX1P0GWpfYlcrHEEbOCKBRhNrzA== + integrity: sha512-uymImpVDTqYc0yiiBEl+t43t7xJ04fVsY/ABz0FzVEHIi16J8EkU86fAyf0Q805sQcP27D3KzA8Fz+B/MygMuA== tarball: 'file:projects/identity.tgz' version: 0.0.0 'file:projects/keyvault-certificates.tgz': @@ -8142,7 +8199,7 @@ packages: dev: false name: '@rush-temp/keyvault-certificates' resolution: - integrity: sha512-rv6xJJmj62sc3W5DNl1z7gXEENkA5sAvIbeMB/UAszqca7sItXiMYutjwuhOFG6qXJpS3NrrSVCG0orB/wjrLw== + integrity: sha512-/EmBup2DREgKOmbgt3fZBhPxwNPqpTdSBWy54pI9q1GjE1J2kNLZORyZ5Na5jW7aaYyz3VRKkU6Fl7SLjQlWNA== tarball: 'file:projects/keyvault-certificates.tgz' version: 0.0.0 'file:projects/keyvault-keys.tgz': @@ -8207,7 +8264,7 @@ packages: dev: false name: '@rush-temp/keyvault-keys' resolution: - integrity: sha512-/PwP2ncF+536NTmI1WYcEpAyFz97PvwXv9KOwVlhZOeewE8XvN6tLG++pE5ZugoTzyQ5pMiMqz4fCYyWwhYcFA== + integrity: sha512-9zTZsDS02bg+1YkTncwJSTLb9qOtWcgbXFgZgnYgKDqSD/yqEGMTpK9Rj5DSBPpupFOrL+MzNNLq6BMHwBhvIw== tarball: 'file:projects/keyvault-keys.tgz' version: 0.0.0 'file:projects/keyvault-secrets.tgz': @@ -8272,7 +8329,7 @@ packages: dev: false name: '@rush-temp/keyvault-secrets' resolution: - integrity: sha512-cJ1e4RAtEhMCc5vOSPam5vF92ra5QMECGnfWrWUY6b2yJvCb5xRw6RKHHEQ12MIhgOhttxChDlkNwMaacfxY7A== + integrity: sha512-USdAavL0C/6SOCNzthn3kEepCGHEk+oi+LUmxr5eGjhThTWjSga/HQVX6olXSbf2mJBcGzJPDr9j+VmerIpURg== tarball: 'file:projects/keyvault-secrets.tgz' version: 0.0.0 'file:projects/logger.tgz': @@ -8325,7 +8382,7 @@ packages: dev: false name: '@rush-temp/logger' resolution: - integrity: sha512-M0JwG2IzUANe8EfHHMsgASl/tKesxTsA38FHqaOrM8B+J3ydK3tR+yoB7b0KNUK7MKTs1rdsOdAoCJVgt9WbMQ== + integrity: sha512-3K9dq+fNmGJNCl6Gf2JoB2I98EIiWTJCniaOPc+kz8Q4GpCASnQWs/attL6pZ2+lvnEu9RRZQC1W7afblnbLTA== tarball: 'file:projects/logger.tgz' version: 0.0.0 'file:projects/search-documents.tgz': @@ -8380,7 +8437,7 @@ packages: dev: false name: '@rush-temp/search-documents' resolution: - integrity: sha512-m1TC1/+0JKVV6OPCM2E3Ik5T99qluuHRaBTBElNcgPQZIuUl2Ma0zhqvg4UyY7xq75eUXBQl29ZruHZTdaDVGw== + integrity: sha512-ckz6alPeV6wV0spD/K6fCTpTYjwRYVwlcL6gO0DSpmiR74PEut2ZnMUofkW4GnhLcXk/sWw/gTGQgOTw6glA8Q== tarball: 'file:projects/search-documents.tgz' version: 0.0.0 'file:projects/service-bus.tgz': @@ -8457,7 +8514,7 @@ packages: dev: false name: '@rush-temp/service-bus' resolution: - integrity: sha512-lmBwlex+ChsbgiBpTxJefk3P08kr/eynRunlcpOde6Di5sTMUItL0M94rfywLtqnWHO4yi/wS5WLRifvGjx+DQ== + integrity: sha512-ZLXrsPk31C1jyfiTc1BS6jTAZFm34ZLJHd/G5OgitFwQxd/q9/RR1bXadrz3VGMWO2WoDkWZQqhSRzJOZdRGzQ== tarball: 'file:projects/service-bus.tgz' version: 0.0.0 'file:projects/storage-blob.tgz': @@ -8518,7 +8575,7 @@ packages: dev: false name: '@rush-temp/storage-blob' resolution: - integrity: sha512-qWjaADsacUBWGIdOmima+OEuBAIy2AN38ehwwYBC0IZu8UARJiEONQN7XJ5iMfAmaFtEaP5zb95K+J1WUqw+yg== + integrity: sha512-ttnEMj0jsm4AtEdUIK+I4ejgdXCuCFHcyX+NsCeWLnwvkVHExi+59TjX/6WUfVmpE9r2l9IWfWE3RuDXFe/ePQ== tarball: 'file:projects/storage-blob.tgz' version: 0.0.0 'file:projects/storage-file-datalake.tgz': @@ -8587,7 +8644,7 @@ packages: dev: false name: '@rush-temp/storage-file-datalake' resolution: - integrity: sha512-ILbrJpnveUsIVdA4vw4gJxD0CE+ki6U8NLBj/7XVrU7Y6oLS4NJ6HKGzCWZ+ZgQEAr4kMhdpN16maUQE7/K+Ag== + integrity: sha512-ObNdwoH59/PQAylgVb3GOU91a7KFqbP5cjxy1JLqIJM1UPolBvke0RSXX2uJoV05afkqKZ+4Ys6sTKdZ2YVIQQ== tarball: 'file:projects/storage-file-datalake.tgz' version: 0.0.0 'file:projects/storage-file-share.tgz': @@ -8648,7 +8705,7 @@ packages: dev: false name: '@rush-temp/storage-file-share' resolution: - integrity: sha512-WSeNkAnchCVy4NUuIUKvxZDv1T13x904ukVoHoxJB6OUzdVeRi4ka+OUuVcivNK/WNcDYrGTWZuWI/hCXwH85Q== + integrity: sha512-5gbuJ4bj1DOznlhGtMVmejHSjEhvClGPZV7U99wxigJYSaQcAmbpU1q1swHTav9bSBsDhLlT4BoEuGOr1Ss6kQ== tarball: 'file:projects/storage-file-share.tgz' version: 0.0.0 'file:projects/storage-queue.tgz': @@ -8708,7 +8765,7 @@ packages: dev: false name: '@rush-temp/storage-queue' resolution: - integrity: sha512-Fjgg+Sl5POgp43Pcxx1M96zqcHyAt5LWcV+qLth0IHX3c1FsLSqJAi5GsuI5yCI4lpbWXCMZBCTC3eslUAhuWQ== + integrity: sha512-L6thmXI7Zx7mtcbW6Yj1ec8u0gioEbTNabUehqxDKS7WmKwvxpL5CML86OJdtxx16GzdkRq5brjzTNtSi/AL0w== tarball: 'file:projects/storage-queue.tgz' version: 0.0.0 'file:projects/template.tgz': @@ -8758,7 +8815,7 @@ packages: dev: false name: '@rush-temp/template' resolution: - integrity: sha512-TDWkHZh1eIHDr5/gjSaLmVfa6BPp79804m33MwChvkJWXYZ6hZJjClcTfIsW9QGcUJUwOQBZTUdd0lq6CqJnGg== + integrity: sha512-qAyshHvyn9Y59JdL19CKh5LVauilbm3DbOlsQrEuHbtleqbrlyfK/yEzH81gNmi+A0jqA3O4ujSJOc9DnO8dyA== tarball: 'file:projects/template.tgz' version: 0.0.0 'file:projects/test-utils-recorder.tgz': @@ -8818,7 +8875,7 @@ packages: dev: false name: '@rush-temp/test-utils-recorder' resolution: - integrity: sha512-2MymjhY0suTq4E9wnY5HNFSla8d3F4dqzQ0w06NJSeLPluhvt2Y6apC3yXwcNRAG9G4YdaQ2IioM1g27l2zhDw== + integrity: sha512-NMKWIl3DskYbKGU+i7lQ03vk+1jY+eGWcp9i0ZNMlnE/yX3lNRzfifNOHDnLoipT7HU5pO/OeiXZE5PhF4JElQ== tarball: 'file:projects/test-utils-recorder.tgz' version: 0.0.0 'file:projects/testhub.tgz': @@ -8839,11 +8896,13 @@ packages: dev: false name: '@rush-temp/testhub' resolution: - integrity: sha512-guDU8PdEdKCVnGxNd1JEkmqukDoc1wodkEqQCWpY1+bX4ZT+ZY520gfVcMeMHYCEO8TAAhScGNke/y7p9qBArA== + integrity: sha512-CW6PxmFjvu2n8k6PvoF5WV4jmri90iw29Trtt8ahDJhd26LPv5c+9bWZTNG46tVsq4KwgRAVNjG6AS2SA1c1LA== tarball: 'file:projects/testhub.tgz' version: 0.0.0 +registry: '' specifiers: '@rush-temp/abort-controller': 'file:./projects/abort-controller.tgz' + '@rush-temp/ai-form-recognizer': 'file:./projects/ai-form-recognizer.tgz' '@rush-temp/ai-text-analytics': 'file:./projects/ai-text-analytics.tgz' '@rush-temp/app-configuration': 'file:./projects/app-configuration.tgz' '@rush-temp/core-amqp': 'file:./projects/core-amqp.tgz' diff --git a/rush.json b/rush.json index 718784bb03c8..8d604afa865a 100644 --- a/rush.json +++ b/rush.json @@ -332,6 +332,11 @@ "projectFolder": "sdk/appconfiguration/app-configuration", "versionPolicyName": "client" }, + { + "packageName": "@azure/ai-form-recognizer", + "projectFolder": "sdk/formrecognizer/ai-form-recognizer", + "versionPolicyName": "client" + }, { "packageName": "@azure/ai-text-analytics", "projectFolder": "sdk/textanalytics/ai-text-analytics", diff --git a/sdk/formrecognizer/ai-form-recognizer/.eslintrc.json b/sdk/formrecognizer/ai-form-recognizer/.eslintrc.json new file mode 100644 index 000000000000..47d9fc35be4a --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "plugins": ["@azure/azure-sdk"], + "extends": ["plugin:@azure/azure-sdk/azure-sdk-base"] +} diff --git a/sdk/formrecognizer/ai-form-recognizer/.vscode/launch.json b/sdk/formrecognizer/ai-form-recognizer/.vscode/launch.json new file mode 100644 index 000000000000..580fe63b1888 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // 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 Program", + "skipFiles": [ + "/**" + ], + "program": "${file}", + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/sdk/formrecognizer/ai-form-recognizer/LICENSE b/sdk/formrecognizer/ai-form-recognizer/LICENSE new file mode 100644 index 000000000000..ea8fb1516028 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sdk/formrecognizer/ai-form-recognizer/README.md b/sdk/formrecognizer/ai-form-recognizer/README.md new file mode 100644 index 000000000000..1c589f75cee4 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/README.md @@ -0,0 +1,260 @@ +# Azure Form Recognizer client library for JavaScript + +[Form Recognizer](https://azure.microsoft.com/services/cognitive-services/form-recognizer/) is a cloud-based service that uses machine learning to extract text and table data from form documents. It allows you to train custom models using your own forms, to extract field names and values, and table data from them. It also provides a prebuilt models you can use to extract values from receipts, or tables from any form. + +[Source code](https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/formrecognizer/ai-form-recognizer/) | +[Package (NPM)](https://www.npmjs.com/package/@azure/ai-form-recognizer) | +[API reference documentation](https://aka.ms/azsdk-js-formrecognizer-ref-docs) | +[Product documentation](https://docs.microsoft.com/azure/cognitive-services/form-recognizer/) | +[Samples](https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/formrecognizer/ai-form-recognizer/samples) + +## Getting started + +### Currently supported environments + +- [Node.js](https://nodejs.org/) version 8.x.x or higher + +### Prerequisites + +- An [Azure subscription][azure_sub]. +- An existing [Cognitive Services][cognitive_resource] or Form Recognizer resource. If you need to create the resource, you can use the [Azure Portal][azure_portal] or [Azure CLI][azure_cli]. + +If you use the Azure CLI, replace `` and `` with your own unique names: + +```PowerShell +az cognitiveservices account create --kind FormRecognizer --resource-group --name +``` + +### Install the `@azure/ai-form-recognizer` package + +Install the Azure Form Recognizer client library for JavaScript with `npm`: + +```bash +npm install @azure/ai-form-recognizer +``` + +### Create and authenticate a client + +In order to interact with the Form Recognizer service, you'll need to select either a `ReceiptClient`, `FormLayoutClient`, or `CustomFormClient`, and create an instance of this type. In the following samples, we will use `CustomFormClient` as an example. To create a client instance to access the Form Recognizer API, you will need the `endpoint` of your Form Recognizer resource and a `credential`. The Form Recognizer client use an API key credential to authenticate. + +You can find the endpoint for your form recognizer resource either in the [Azure Portal][azure_portal] or by using the [Azure CLI][azure_cli] snippet below: + +```bash +az cognitiveservices account show --name --resource-group --query "endpoint" +``` + +#### Using an API Key + +Use the [Azure Portal][azure_portal] to browse to your Form Recognizer resource and retrieve an API key, or use the [Azure CLI][azure_cli] snippet below: + +**Note:** Sometimes the API key is referred to as a "subscription key" or "subscription API key." + +```PowerShell +az cognitiveservices account keys list --resource-group --name +``` + +Once you have an API key and endpoint, you can use it as follows: + +```js +const { CustomFormClient, ApiKeyCredential } = require("@azure/ai-form-recognizer"); + +const client = new CustomFormClient( + "", + new ApiKeyCredential("") +); +``` + +## Key concepts + +### ReceiptClient +A `ReceiptClient` is the Form Recognizer interface to use for analyzing receipts. It provides operations to extract receipt field values and locations from receipts from the United States. + +### FormLayoutClient +A `FormLayoutClient` is the Form Recognizer interface to extract layout items from forms. It provides operations to extract table data and geometry. + +### CustomFormClient +A `CustomFormClient` is the Form Recognizer interface to use for creating, using, and managing custom machine-learned models. It provides operations for training models on forms you provide, and extracting field values and locations from your custom forms. It also provides operations for viewing and deleting models, as well as understanding how close you are to reaching subscription limits for the number of models you can train. + +### Long-Running Operations +Long-running operations are operations which consist of an initial request sent to the service to start an operation,followed by polling the service at intervals to determine whether the operation has completed or failed, and if it has succeeded, to get the result. + +Methods that train models or extract values from forms are modeled as long-running operations. The client exposes a `begin` method that returns an `Promise`. Callers should wait for the operation to complete by calling `pollUntilDone()` on the poller returned from the `begin` method. A sample code snippet is provided to illustrate using long-running operations [below](#extracting-receipt-values-with-a-long-running-operation). + +### Training models +Using the `CustomFormClient`, you can train a machine-learned model on your own form type. The resulting model will be able to extract values from the types of forms it was trained on. + +#### Training without labels +A model trained without labels uses unsupervised learning to understand the layout and relationships between field names and values in your forms. The learning algorithm clusters the training forms by type and learns what fields and tables are present in each form type. + +This approach doesn't require manual data labeling or intensive coding and maintenance, and we recommend you try this method first when training custom models. + +#### Training with labels +A model trained with labels uses supervised learning to extract values you specify by adding labels to your training forms. The learning algorithm uses a label file you provide to learn what fields are found at various locations in the form, and learns to extract just those values. + +This approach can result in better-performing models, and those models can work with more complex form structures. + +### Extracting values from forms +Using the `CustomFormClient`, you can use your own trained models to extract field values and locations, as well as table data, from forms of the type you trained your models on. The output of models trained with and without labels differs as described below. + +#### Using models trained without labels +Models trained without labels consider each form page to be a different form type. For example, if you train your model on 3-page forms, it will learn that these are three different types of forms. When you send a form to it for analysis, it will return a collection of three pages, where each page contains the field names, values, and locations, as well as table data, found on that page. + +#### Using models trained with labels +Models trained with labels consider a form as a single unit. For example, if you train your model on 3-page forms with labels, it will learn to extract field values from the locations you've labeled across all pages in the form. If you sent a document containing two forms to it for analysis, it would return a collection of two forms, where each form contains the field names, values, and locations, as well as table data, found in that form. Fields and tables have page numbers to identify the pages where they were found. + +### Managing Custom Models +Using the `CustomFormClient`, you can get, list, and delete the custom models you've trained. You can also view the count of models you've trained and the maximum number of models your subscription will allow you to store. + + +## Examples +The following section provides several code snippets illustrating common patterns used in the Form Recognizer client libraries. + +### Extracting receipt values with a long-running operation + +```javascript +const { ReceiptRecognizerClient, FormRecognizerApiKeyCredential } = require("@azure/ai-form-recognizer"); +const fs = require("fs"); + +require("dotenv").config(); + +async function main() { + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const path = "./contoso-allinone.jpg"; + const readStream = fs.createReadStream(path); + + const client = new ReceiptRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + // start a long-running operation (LRO) to extract receipt data + const poller = await client.beginExtractReceipts(readStream, { + includeTextDetails: true, + onProgress: (state) => { console.log(`analyzing status: ${state.status}`); } + }); + await poller.pollUntilDone(); + response = poller.getResult(); + + console.log("### First receipt:") + console.log(response.extractedReceipts[0]); + console.log("### Items:") + console.log("### First receipt:") + console.log(response.extractedReceipts[0]); + console.log("### Items:") + console.table(response.extractedReceipts[0].items, ["name", "quantity", "price", "totalPrice"]); + console.log("### Raw 'MerchantAddress' fields:"); + console.log(response.extractedReceipts[0].fields["MerchantAddress"]) +} + +main(); +``` + +### Training models + +```javascript +const { FormRecognizerClient, FormRecognizerApiKeyCredential } = require("@azure/ai-form-recognizer"); + +async function main() { + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const trainingDataSource = process.env["DOCUMENT_SOURCE"] || ""; + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + // start a long-running operation (LRO) to train the model + const poller = await client.beginTraining(trainingDataSource, { + onProgress: (state) => { console.log(`training status: ${state.status}`); } + }); + await poller.pollUntilDone(); + const model = poller.getResult(); + console.log(model); +} + +main(); +``` + +### Listing all models in the current cognitive service account + +```javascript +const { FormRecognizerClient, FormRecognizerApiKeyCredential } = require("../../dist"); + +async function main() { + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + + // returns an async iteratable iterator that supports paging + const result = await client.listModels(); + let i = 0; + for await (const modelInfo of result) { + console.log(`model ${i++}:`); + console.log(modelInfo); + } + + // using `iter.next()` + i = 1; + let iter = client.listModels(); + let modelItem = await iter.next(); + while (!modelItem.done) { + console.log(`model ${i++}: ${modelItem.value.modelId}`); + modelItem = await iter.next(); + } + + // using `byPage()` + i = 1; + for await (const response of client.listModels().byPage()) { + for (const modelInfo of response.modelList) { + console.log(`model ${i++}: ${modelInfo.modelId}`); + } + } +} + +main(); +``` + +## Troubleshooting + +### Enable logs + +You can set the following environment variable to see debug logs when using this library. + +- Getting debug logs from the Azure Form Recognizer client library + +```bash +export DEBUG=azure* +``` + +For more detailed instructions on how to enable logs, you can look at the [@azure/logger package docs](https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/core/logger). + +## Next steps + +Please take a look at the +[samples](https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/formrecognizer/ai-form-recognizer/samples) +directory for detailed examples on how to use this library. + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +If you'd like to contribute to this library, please read the [contributing guide](https://github.com/Azure/azure-sdk-for-js/blob/master/CONTRIBUTING.md) to learn more about how to build and test the code. + +## Related projects + +- [Microsoft Azure SDK for Javascript](https://github.com/Azure/azure-sdk-for-js) + +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-js%2Fsdk%2Fformrecognizer%2Fai-form-recognizer%2FREADME.png) + +[azure_cli]: https://docs.microsoft.com/cli/azure +[azure_sub]: https://azure.microsoft.com/free/ +[cognitive_resource]: https://docs.microsoft.com/en-us/azure/cognitive-services/cognitive-services-apis-create-account +[azure_portal]: https://portal.azure.com +[azure_identity]: https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/identity/identity +[cognitive_auth]: https://docs.microsoft.com/azure/cognitive-services/authentication +[register_aad_app]: https://docs.microsoft.com/azure/cognitive-services/authentication#assign-a-role-to-a-service-principal +[defaultazurecredential]: https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/identity/identity#defaultazurecredential diff --git a/sdk/formrecognizer/ai-form-recognizer/api-extractor.json b/sdk/formrecognizer/ai-form-recognizer/api-extractor.json new file mode 100644 index 000000000000..ebfdc7a5088d --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/api-extractor.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "types/src/index.d.ts", + "docModel": { + "enabled": false + }, + "apiReport": { + "enabled": true, + "reportFolder": "./review" + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "", + "publicTrimmedFilePath": "./types/ai-form-recognizer.d.ts" + }, + "messages": { + "tsdocMessageReporting": { + "default": { + "logLevel": "none" + } + }, + "extractorMessageReporting": { + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-unresolved-link": { + "logLevel": "none" + } + } + } +} diff --git a/sdk/formrecognizer/ai-form-recognizer/karma.conf.js b/sdk/formrecognizer/ai-form-recognizer/karma.conf.js new file mode 100644 index 000000000000..7aace85f782e --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/karma.conf.js @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// https://github.com/karma-runner/karma-chrome-launcher +process.env.CHROME_BIN = require("puppeteer").executablePath(); +require("dotenv").config(); +const { + jsonRecordingFilterFunction, + isPlaybackMode, + isSoftRecordMode, + isRecordMode +} = require("@azure/test-utils-recorder"); + +module.exports = function(config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: "./", + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ["mocha"], + + plugins: [ + "karma-mocha", + "karma-mocha-reporter", + "karma-chrome-launcher", + "karma-edge-launcher", + "karma-firefox-launcher", + "karma-ie-launcher", + "karma-env-preprocessor", + "karma-coverage", + "karma-remap-istanbul", + "karma-junit-reporter", + "karma-json-to-file-reporter", + "karma-json-preprocessor" + ], + + // list of files / patterns to load in the browser + files: ["dist-test/index.browser.js"].concat( + isPlaybackMode() || isSoftRecordMode() ? ["recordings/browsers/**/*.json"] : [] + ), + + // list of files / patterns to exclude + exclude: [], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + "**/*.js": ["env"], + "recordings/browsers/**/*.json": ["json"] + // IMPORTANT: COMMENT following line if you want to debug in your browsers!! + // Preprocess source file to calculate code coverage, however this will make source file unreadable + // "test-browser/index.js": ["coverage"] + }, + + envPreprocessor: [ + "TEST_MODE", + "ENDPOINT", + "FORM_RECOGNIZER_API_KEY", + "FORM_RECOGNIZER_API_KEY_ALT", + "AZURE_CLIENT_ID", + "AZURE_CLIENT_SECRET", + "AZURE_TENANT_ID" + ], + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ["mocha", "coverage", "karma-remap-istanbul", "junit", "json-to-file"], + + coverageReporter: { + // specify a common output directory + dir: "coverage-browser/", + reporters: [{ type: "json", subdir: ".", file: "coverage.json" }] + }, + + remapIstanbulReporter: { + src: "coverage-browser/coverage.json", + reports: { + lcovonly: "coverage-browser/lcov.info", + html: "coverage-browser/html/report", + "text-summary": null, + cobertura: "./coverage-browser/cobertura-coverage.xml" + } + }, + + junitReporter: { + outputDir: "", // results will be saved as $outputDir/$browserName.xml + outputFile: "test-results.browser.xml", // if included, results will be saved as $outputDir/$browserName/$outputFile + suite: "", // suite will become the package name attribute in xml testsuite element + useBrowserName: false, // add browser name to report and classes names + nameFormatter: undefined, // function (browser, result) to customize the name attribute in xml testcase element + classNameFormatter: undefined, // function (browser, result) to customize the classname attribute in xml testcase element + properties: {} // key value pair of properties to add to the section of the report + }, + + jsonToFileReporter: { + filter: jsonRecordingFilterFunction, + outputPath: "." + }, + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // --no-sandbox allows our tests to run in Linux without having to change the system. + // --disable-web-security allows us to authenticate from the browser without having to write tests using interactive auth, which would be far more complex. + browsers: ["ChromeHeadlessNoSandbox"], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox", "--disable-web-security"] + } + }, + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: 1, + + browserNoActivityTimeout: 600000, + browserDisconnectTimeout: 10000, + browserDisconnectTolerance: 3, + browserConsoleLogOptions: { + terminal: !isRecordMode() + }, + + client: { + mocha: { + // change Karma's debug.html to the mocha web reporter + reporter: "html", + timeout: "600000" + } + } + }); +}; diff --git a/sdk/formrecognizer/ai-form-recognizer/package.json b/sdk/formrecognizer/ai-form-recognizer/package.json new file mode 100644 index 000000000000..3137a019b1a5 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/package.json @@ -0,0 +1,136 @@ +{ + "name": "@azure/ai-form-recognizer", + "sdk-type": "client", + "author": "Microsoft Corporation", + "description": "An isomorphic client library for the Azure Form Recognizer service.", + "version": "1.0.0-preview.1", + "keywords": [ + "node", + "azure", + "typescript", + "browser", + "isomorphic", + "cloud" + ], + "license": "MIT", + "main": "./dist/index.js", + "module": "./dist-esm/src/index.js", + "browser": { + "./dist-esm/src/utils/utils.node.js": "./dist-esm/src/utils/utils.browser.js" + }, + "types": "./types/ai-form-recognizer.d.ts", + "homepage": "https://github.com/azure/azure-sdk-for-js/tree/master/sdk/formrecognizers/ai-form-recognizer", + "repository": "github:Azure/azure-sdk-for-js", + "bugs": { + "url": "https://github.com/Azure/azure-sdk-for-js/issues" + }, + "files": [ + "dist/", + "dist-esm/src/", + "types/ai-form-recognizer.d.ts", + "README.md", + "LICENSE" + ], + "//metadata": { + "constantPaths": [ + { + "path": "src/generated/formRecognizerClientContext.ts", + "prefix": "packageVersion" + }, + { + "path": "src/constants.ts", + "prefix": "SDK_VERSION" + } + ] + }, + "scripts": { + "audit": "node ../../../common/scripts/rush-audit.js && rimraf node_modules package-lock.json && npm i --package-lock-only 2>&1 && npm audit", + "build:autorest": "autorest ./swagger/README.md --typescript --package-version=1.0.0-preview.1 --version=3.0.6258", + "build:browser": "tsc -p . && cross-env ONLY_BROWSER=true rollup -c 2>&1", + "build:node": "tsc -p . && cross-env ONLY_NODE=true rollup -c 2>&1", + "build:samples": "cd samples && tsc -p .", + "build:test": "tsc -p . && rollup -c rollup.test.config.js 2>&1", + "build": "tsc -p . && rollup -c 2>&1 && api-extractor run --local", + "check-format": "prettier --list-different --config ../../.prettierrc.json \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json}\"", + "clean": "rimraf dist dist-esm dist-browser test-dist temp types *.tgz *.log", + "execute:samples": "echo skipped", + "extract-api": "tsc -p . && api-extractor run --local", + "format": "prettier --write --config ../../.prettierrc.json \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json}\"", + "integration-test:browser": "echo skipped", + "integration-test:node": "echo skipped", + "integration-test": "npm run integration-test:node && npm run integration-test:browser", + "lint:fix": "eslint package.json tsconfig.json api-extractor.json src test --ext .ts --fix --fix-type [problem,suggestion]", + "lint": "eslint package.json tsconfig.json api-extractor.json src test --ext .ts -f html -o formrecognizer-lintReport.html || exit 0", + "pack": "npm pack 2>&1", + "prebuild": "npm run clean", + "test:browser": "npm run build:test && npm run unit-test:browser && npm run integration-test:browser", + "test:node": "npm run build:test && npm run unit-test:node && npm run integration-test:node", + "test": "npm run build:test && npm run unit-test && npm run integration-test", + "unit-test:browser": "karma start --single-run", + "unit-test:node": "mocha --require source-map-support/register --reporter ../../../common/tools/mocha-multi-reporter.js --timeout 120000 --full-trace dist-test/index.node.js", + "unit-test": "npm run unit-test:node && npm run unit-test:browser" + }, + "sideEffects": false, + "autoPublish": false, + "prettier": "@azure/eslint-plugin-azure-sdk/prettier.json", + "dependencies": { + "@azure/core-auth": "^1.1.0", + "@azure/core-lro": "^1.0.0", + "@azure/core-paging": "^1.1.0", + "@azure/core-http": "^1.0.0", + "@azure/core-tracing": "1.0.0-preview.7", + "@azure/logger": "^1.0.0", + "@opentelemetry/types": "^0.2.0", + "tslib": "^1.10.0" + }, + "devDependencies": { + "@azure/eslint-plugin-azure-sdk": "^3.0.0", + "@azure/identity": "1.1.0-preview.2", + "@azure/test-utils-recorder": "^1.0.0", + "@microsoft/api-extractor": "^7.5.4", + "@rollup/plugin-commonjs": "^11.0.1", + "@rollup/plugin-json": "^4.0.0", + "@rollup/plugin-multi-entry": "^3.0.0", + "@rollup/plugin-node-resolve": "^7.0.0", + "@rollup/plugin-replace": "^2.2.0", + "@types/chai": "^4.1.6", + "@types/mocha": "^7.0.2", + "@types/node": "^8.0.0", + "@types/sinon": "^7.0.13", + "@typescript-eslint/eslint-plugin": "^2.0.0", + "@typescript-eslint/parser": "^2.0.0", + "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", + "cross-env": "^6.0.3", + "dotenv": "^8.2.0", + "eslint": "^6.1.0", + "eslint-config-prettier": "^6.0.0", + "eslint-plugin-no-null": "^1.0.2", + "eslint-plugin-no-only-tests": "^2.3.0", + "eslint-plugin-promise": "^4.1.1", + "karma": "^4.0.1", + "karma-chrome-launcher": "^3.0.0", + "karma-coverage": "^2.0.0", + "karma-edge-launcher": "^0.4.2", + "karma-env-preprocessor": "^0.1.1", + "karma-firefox-launcher": "^1.1.0", + "karma-ie-launcher": "^1.0.0", + "karma-json-preprocessor": "^0.3.3", + "karma-json-to-file-reporter": "^1.0.1", + "karma-junit-reporter": "^2.0.1", + "karma-mocha": "^1.3.0", + "karma-mocha-reporter": "^2.2.5", + "karma-remap-istanbul": "^0.6.0", + "mocha": "^7.1.1", + "mocha-junit-reporter": "^1.18.0", + "prettier": "^1.16.4", + "rimraf": "^3.0.0", + "rollup": "^1.16.3", + "rollup-plugin-sourcemaps": "^0.4.2", + "rollup-plugin-terser": "^5.1.1", + "rollup-plugin-visualizer": "^3.1.1", + "sinon": "^7.1.0", + "source-map-support": "^0.5.9", + "typescript": "~3.7.5" + } +} diff --git a/sdk/formrecognizer/ai-form-recognizer/review/ai-form-recognizer.api.md b/sdk/formrecognizer/ai-form-recognizer/review/ai-form-recognizer.api.md new file mode 100644 index 000000000000..70c0c019219a --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/review/ai-form-recognizer.api.md @@ -0,0 +1,665 @@ +## API Report File for "@azure/ai-form-recognizer" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { AbortSignalLike } from '@azure/core-http'; +import * as coreHttp from '@azure/core-http'; +import { OperationOptions } from '@azure/core-http'; +import { PagedAsyncIterableIterator } from '@azure/core-paging'; +import { PipelineOptions } from '@azure/core-http'; +import { PollerLike } from '@azure/core-lro'; +import { PollOperationState } from '@azure/core-lro'; +import { RestResponse } from '@azure/core-http'; +import { ServiceClientCredentials } from '@azure/core-http'; +import { WebResource } from '@azure/core-http'; + +// @public +export interface AnalyzeOperationResultModel { + // Warning: (ae-forgotten-export) The symbol "AnalyzeResult" needs to be exported by the entry point index.d.ts + analyzeResult?: AnalyzeResult; + createdOn: Date; + lastUpdatedOn: Date; + status: OperationStatus; +} + +// @public +export interface ArrayFieldValue { + // (undocumented) + type: "array"; + // (undocumented) + value?: FieldValue[]; +} + +// @public +export type BeginRecognizeContentOptions = RecognizeContentOptions & { + intervalInMs?: number; + onProgress?: (state: BeginRecognizePollState) => void; + resumeFrom?: string; +}; + +// @public +export type BeginRecognizeFormsOptions = RecognizeFormsOptions & { + intervalInMs?: number; + onProgress?: (state: BeginRecognizePollState) => void; + resumeFrom?: string; +}; + +// @public +export type BeginRecognizeLabeledFormOptions = RecognizeFormsOptions & { + intervalInMs?: number; + onProgress?: (state: BeginRecognizePollState) => void; + resumeFrom?: string; +}; + +// @public +export type BeginRecognizeReceiptsOptions = RecognizeReceiptsOptions & { + intervalInMs?: number; + onProgress?: (state: BeginRecognizePollState) => void; + resumeFrom?: string; +}; + +// @public +export type BeginTrainingOptions = TrainModelOptions & { + intervalInMs?: number; + onProgress?: (state: BeginTrainingPollState) => void; + resumeFrom?: string; +}; + +// @public +export type BeginTrainingWithLabelsOptions = FormRecognizerOperationOptions & { + prefix?: string; + includeSubFolders?: boolean; +}; + +// @public +export interface CommonFieldValue { + boundingBox?: Point2D[]; + confidence?: number; + elements?: FormElement[]; + pageNumber?: number; + text?: string; +} + +// @public +export type ContentPollerLike = PollerLike, RecognizeContentResultResponse>; + +// @public +export type ContentType = "application/pdf" | "image/jpeg" | "image/png" | "image/tiff"; + +// @public +export type DateFieldValue = { + type: "date"; + value?: Date; +} & CommonFieldValue; + +// @public +export type DeleteModelOptions = FormRecognizerOperationOptions; + +// @public (undocumented) +export interface ErrorInformation { + // (undocumented) + code: string; + // (undocumented) + message: string; +} + +// @public +export type FieldValue = StringFieldValue | DateFieldValue | TimeFieldValue | PhoneNumberFieldValue | NumberFieldValue | IntegerFieldValue | ArrayFieldValue | ObjectFieldValue; + +// @public +export type FormElement = FormWord | FormLine; + +// @public +export interface FormElementCommon { + boundingBox: Point2D[]; + pageNumber: number; + text: string; +} + +// @public +export interface FormField { + confidence: number; + fieldLabel: FormText; + label?: string; + valueText: FormText; +} + +// @public +export interface FormFieldsReport { + accuracy: number; + fieldName: string; +} + +// @public +export interface FormLine extends FormElementCommon { + kind: "line"; + words: FormWord[]; +} + +// @public +export interface FormModel { + keys: KeysResult; + modelInfo: ModelInfo; + trainResult?: FormTrainResult; +} + +// @public +export type FormModelResponse = FormModel & { + _response: coreHttp.HttpResponse & { + bodyAsText: string; + parsedBody: Model; + }; +}; + +// @public +export interface FormPage { + angle: number; + height: number; + lines?: FormLine[]; + pageNumber: number; + unit: LengthUnit; + width: number; +} + +// @public +export interface FormPageRange { + firstPageNumber: number; + lastPageNumber: number; +} + +// @public +export type FormPollerLike = PollerLike, RecognizeFormResultResponse>; + +// @public +export class FormRecognizerApiKeyCredential implements ServiceClientCredentials { + constructor(apiKey: string); + signRequest(webResource: WebResource): Promise; + updateKey(apiKey: string): void; +} + +// @public +export class FormRecognizerClient { + constructor(endpointUrl: string, credential: FormRecognizerApiKeyCredential, options?: FormRecognizerClientOptions); + beginRecognizeContent(source: FormRecognizerRequestBody, contentType?: ContentType, options?: BeginRecognizeContentOptions): Promise; + // (undocumented) + beginRecognizeContentFromUrl(documentUrl: string, options?: BeginRecognizeContentOptions): Promise; + beginRecognizeForms(modelId: string, body: FormRecognizerRequestBody, contentType?: ContentType, options?: BeginRecognizeFormsOptions): Promise; + // (undocumented) + beginRecognizeFormsFromUrl(modelId: string, documentUrl: string, options?: BeginRecognizeFormsOptions): Promise, RecognizeFormResultResponse>>; + beginRecognizeLabeledForms(modelId: string, body: FormRecognizerRequestBody, contentType: ContentType, options?: BeginRecognizeLabeledFormOptions): Promise; + // (undocumented) + beginRecognizeLabeledFormsFromUrl(modelId: string, documentUrl: string, options?: BeginRecognizeLabeledFormOptions): Promise, LabeledFormResultResponse>>; + beginRecognizeReceipts(source: FormRecognizerRequestBody, contentType?: ContentType, options?: BeginRecognizeReceiptsOptions): Promise; + beginRecognizeReceiptsFromUrl(documentUrl: string, options?: BeginRecognizeReceiptsOptions): Promise; + readonly endpointUrl: string; + getFormTrainingClient(): FormTrainingClient; + } + +// @public +export interface FormRecognizerClientOptions extends PipelineOptions { +} + +// @public +export interface FormRecognizerOperationOptions extends OperationOptions { +} + +// @public +export type FormRecognizerRequestBody = Blob | ArrayBuffer | ArrayBufferView | NodeJS.ReadableStream; + +// @public +export interface FormResult { + errors?: ErrorInformation[]; + extractedPages?: RecognizedPage[]; + rawExtractedPages: FormPage[]; + version: string; +} + +// @public +export interface FormTable { + columnCount: number; + rowCount: number; + rows: FormTableRow[]; +} + +// @public +export interface FormTableCell { + boundingBox: Point2D[]; + columnIndex: number; + columnSpan?: number; + confidence: number; + elements?: FormElement[]; + isFooter?: boolean; + isHeader?: boolean; + rowIndex: number; + rowSpan?: number; + text: string; +} + +// @public +export interface FormTableRow { + cells: FormTableCell[]; +} + +// @public +export interface FormText { + boundingBox?: Point2D[]; + elements?: FormElement[]; + text: string; +} + +// @public +export class FormTrainingClient { + constructor(endpointUrl: string, credential: FormRecognizerApiKeyCredential, options?: FormRecognizerClientOptions); + beginTraining(source: string, options?: BeginTrainingOptions): Promise, FormModelResponse>>; + beginTrainingWithLabel(source: string, options?: BeginTrainingOptions): Promise, LabeledFormModelResponse>>; + deleteModel(modelId: string, options?: DeleteModelOptions): Promise; + readonly endpointUrl: string; + getLabeledModel(modelId: string, options?: GetLabeledModelOptions): Promise; + getModel(modelId: string, options?: GetModelOptions): Promise; + getSummary(options?: GetSummaryOptions): Promise; + listModels(options?: ListModelsOptions): PagedAsyncIterableIterator; + } + +// @public +export interface FormTrainResult { + errors?: ErrorInformation[]; + trainingDocuments: TrainingDocumentInfo[]; +} + +// @public +export interface FormWord extends FormElementCommon { + confidence?: number; + containingLine?: FormLine; + kind: "word"; +} + +// @public +export type GetLabeledModelOptions = FormRecognizerOperationOptions & { + includeKeys?: boolean; +}; + +// @public +export type GetModelOptions = FormRecognizerOperationOptions; + +// @public +export type GetSummaryOptions = FormRecognizerOperationOptions; + +// @public +export type IntegerFieldValue = { + type: "integer"; + value?: number; +} & CommonFieldValue; + +// @public +export interface KeysResult { + clusters: { + [propertyName: string]: string[]; + }; +} + +// @public +export interface KeyValueElementModel { + boundingBox?: number[]; + elements?: string[]; + text: string; +} + +// @public +export interface KeyValuePairModel { + confidence: number; + key: KeyValueElementModel; + label?: string; + value: KeyValueElementModel; +} + +// @public +export interface LabeledFormModel { + modelInfo: ModelInfo; + trainResult?: LabeledFormTrainResult; +} + +// @public +export type LabeledFormModelResponse = LabeledFormModel & { + _response: coreHttp.HttpResponse & { + bodyAsText: string; + parsedBody: Model; + }; +}; + +// @public +export type LabeledFormOperationResult = Partial & { + status: OperationStatus; + createdOn: Date; + lastUpdatedOn: Date; +}; + +// @public +export type LabeledFormPollerLike = PollerLike, LabeledFormResultResponse>; + +// @public +export interface LabeledFormResult { + errors?: ErrorInformation[]; + extractedForms?: RecognizedForm[]; + extractedPages?: RecognizedPage[]; + rawExtractedPages: FormPage[]; + version: string; +} + +// @public +export type LabeledFormResultResponse = LabeledFormOperationResult & { + _response: coreHttp.HttpResponse & { + bodyAsText: string; + parsedBody: AnalyzeOperationResultModel; + }; +}; + +// @public +export interface LabeledFormTrainResult { + averageModelAccuracy: number; + errors?: ErrorInformation[]; + fields: FormFieldsReport[]; + trainingDocuments: TrainingDocumentInfo[]; +} + +// @public +export type Language = "en" | "es"; + +// @public +export type LengthUnit = "pixel" | "inch"; + +// @public +export type ListModelsOptions = FormRecognizerOperationOptions; + +// @public +export type ListModelsResponseModel = Models & { + _response: coreHttp.HttpResponse & { + bodyAsText: string; + parsedBody: Models; + }; +}; + +// @public +export interface Model { + keys?: KeysResult; + modelInfo: ModelInfo; + trainResult?: TrainResult; +} + +// @public +export interface ModelInfo { + createdOn: Date; + lastUpdatedOn: Date; + modelId: string; + status: ModelStatus; +} + +// @public +export interface Models { + modelList?: ModelInfo[]; + nextLink?: string; + summary?: ModelsSummary; +} + +// @public +export interface ModelsSummary { + count: number; + lastUpdatedOn: Date; + limit: number; +} + +// @public +export type ModelStatus = "creating" | "ready" | "invalid"; + +// @public +export type NumberFieldValue = { + type: "number"; + value?: number; +} & CommonFieldValue; + +// @public +export interface ObjectFieldValue { + // (undocumented) + type: "object"; + // (undocumented) + value?: { + [propertyName: string]: FieldValue; + }; +} + +// @public +export type OperationStatus = "notStarted" | "running" | "succeeded" | "failed"; + +// @public +export type PhoneNumberFieldValue = { + type: "phoneNumber"; + value?: string; +} & CommonFieldValue; + +// @public +export interface Point2D { + x: number; + y: number; +} + +export { PollerLike } + +export { PollOperationState } + +// @public +export interface RawReceiptResult { + docType: "prebuilt:receipt"; + fields: { + [propertyName: string]: FieldValue; + }; + pageRange: FormPageRange; +} + +// @public +export interface RawUSReceipt { + Items: ReceiptItemArrayField; + MerchantAddress: StringFieldValue; + MerchantName: StringFieldValue; + MerchantPhoneNumber: PhoneNumberFieldValue; + ReceiptType: StringFieldValue; + Subtotal: NumberFieldValue; + Tax: NumberFieldValue; + Tip: NumberFieldValue; + Total: NumberFieldValue; + TransactionDate: DateFieldValue; + TransactionTime: TimeFieldValue; +} + +// @public +export interface Receipt { + items: ReceiptItem[]; + merchantAddress?: string; + merchantName?: string; + merchantPhoneNumber?: string; + receiptType: string; + subtotal?: number; + tax?: number; + tip?: number; + total?: number; + transactionDate?: Date; + transactionTime?: string; +} + +// @public +export interface ReceiptItem { + name?: string; + price?: number; + quantity?: number; + totalPrice?: number; +} + +// @public +export interface ReceiptItemArrayField { + // (undocumented) + type: "array"; + // (undocumented) + value: ReceiptItemField[]; +} + +// @public +export type ReceiptItemField = { + type: "object"; + value: { + Name: StringFieldValue; + Quantity: NumberFieldValue; + Price: NumberFieldValue; + TotalPrice: NumberFieldValue; + }; +} & CommonFieldValue; + +// @public +export type ReceiptPollerLike = PollerLike, RecognizeReceiptResultResponse>; + +// @public +export type RecognizeContentOperationResult = { + status: OperationStatus; + createdOn: Date; + lastUpdatedOn: Date; +} & Partial; + +// @public +export type RecognizeContentOptions = FormRecognizerOperationOptions; + +// @public +export type RecognizeContentResultResponse = RecognizeContentOperationResult & { + _response: coreHttp.HttpResponse & { + bodyAsText: string; + parsedBody: AnalyzeOperationResultModel; + }; +}; + +// @public +export interface RecognizedContent { + extractedLayoutPages?: RecognizedContentPage[]; + rawExtractedPages: FormPage[]; + version: string; +} + +// @public +export interface RecognizedContentPage { + fields?: FormField[]; + pageNumber: number; + tables?: FormTable[]; +} + +// @public +export interface RecognizedForm { + docType: string; + fields: { + [propertyName: string]: FieldValue; + }; + pageRange: FormPageRange; +} + +// @public +export interface RecognizedPage { + fields?: FormField[]; + formTypeId?: number; + pageNumber: number; + tables?: FormTable[]; +} + +// @public +export type RecognizedReceipt = RawReceiptResult & Receipt; + +// @public +export type RecognizeFormOperationResult = Partial & { + status: OperationStatus; + createdOn: Date; + lastUpdatedOn: Date; +}; + +// @public +export type RecognizeFormResultResponse = RecognizeFormOperationResult & { + _response: coreHttp.HttpResponse & { + bodyAsText: string; + parsedBody: AnalyzeOperationResultModel; + }; +}; + +// @public +export type RecognizeFormsOptions = FormRecognizerOperationOptions & { + includeTextDetails?: boolean; +}; + +// @public +export type RecognizeReceiptOperationResult = { + status: OperationStatus; + createdOn: Date; + lastUpdatedOn: Date; +} & Partial; + +// @public +export interface RecognizeReceiptResult { + extractedReceipts?: RecognizedReceipt[]; + rawExtractedPages: FormPage[]; + version: string; +} + +// @public +export type RecognizeReceiptResultResponse = RecognizeReceiptOperationResult & { + _response: coreHttp.HttpResponse & { + bodyAsText: string; + parsedBody: AnalyzeOperationResultModel; + }; +}; + +// @public +export type RecognizeReceiptsOptions = FormRecognizerOperationOptions & { + includeTextDetails?: boolean; +}; + +export { RestResponse } + +// @public +export type StringFieldValue = { + type: "string"; + value?: string; +} & CommonFieldValue; + +// @public +export type TimeFieldValue = { + type: "time"; + value?: string; +} & CommonFieldValue; + +// @public +export interface TrainingDocumentInfo { + documentName: string; + errors: ErrorInformation[]; + pages: number; + status: TrainStatus; +} + +// @public +export type TrainModelOptions = FormRecognizerOperationOptions & { + prefix?: string; + includeSubFolders?: boolean; +}; + +// @public +export interface TrainResult { + averageModelAccuracy?: number; + errors?: ErrorInformation[]; + fields?: FormFieldsReport[]; + trainingDocuments: TrainingDocumentInfo[]; +} + +// @public +export type TrainStatus = "succeeded" | "partiallySucceeded" | "failed"; + + +// Warnings were encountered during analysis: +// +// src/formRecognizerClient.ts:74:3 - (ae-forgotten-export) The symbol "BeginRecognizePollState" needs to be exported by the entry point index.d.ts +// src/formTrainingClient.ts:74:3 - (ae-forgotten-export) The symbol "BeginTrainingPollState" needs to be exported by the entry point index.d.ts + +// (No @packageDocumentation comment for this package) + +``` diff --git a/sdk/formrecognizer/ai-form-recognizer/rollup.base.config.js b/sdk/formrecognizer/ai-form-recognizer/rollup.base.config.js new file mode 100644 index 000000000000..505306ca0cc7 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/rollup.base.config.js @@ -0,0 +1,122 @@ +import path from "path"; +import nodeResolve from "@rollup/plugin-node-resolve"; +import multiEntry from "@rollup/plugin-multi-entry"; +import cjs from "@rollup/plugin-commonjs"; +import replace from "@rollup/plugin-replace"; +import { terser } from "rollup-plugin-terser"; +import sourcemaps from "rollup-plugin-sourcemaps"; +import viz from "rollup-plugin-visualizer"; + +const pkg = require("./package.json"); +const depNames = Object.keys(pkg.dependencies); +const devDepNames = Object.keys(pkg.devDependencies); +const input = "dist-esm/src/index.js"; +const production = process.env.NODE_ENV === "production"; + +export function nodeConfig(test = false) { + const externalNodeBuiltins = [ "stream" ]; + const baseConfig = { + input: input, + external: depNames.concat(externalNodeBuiltins), + output: { file: "dist/index.js", format: "cjs", sourcemap: true }, + preserveSymlinks: false, + plugins: [ + sourcemaps(), + replace({ + delimiters: ["", ""], + values: { + // replace dynamic checks with if (true) since this is for node only. + // Allows rollup's dead code elimination to be more aggressive. + "if (isNode)": "if (true)" + } + }), + nodeResolve({ preferBuiltins: true }), + cjs() + ] + }; + + if (test) { + // Entry points - test files under the `test` folder(common for both browser and node), node specific test files + baseConfig.input = ["dist-esm/test/*.spec.js", "dist-esm/test/node/*.spec.js"]; + baseConfig.plugins.unshift(multiEntry({ exports: false })); + + // different output file + baseConfig.output.file = "dist-test/index.node.js"; + + // mark devdeps as external + baseConfig.external.push(...devDepNames); + + // Disable tree-shaking of test code. In rollup-plugin-node-resolve@5.0.0, rollup started respecting + // the "sideEffects" field in package.json. Since our package.json sets "sideEffects=false", this also + // applies to test code, which causes all tests to be removed by tree-shaking. + baseConfig.treeshake = false; + } else if (production) { + baseConfig.plugins.push(terser()); + } + + return baseConfig; +} + +export function browserConfig(test = false) { + const baseConfig = { + input: input, + output: { + file: "dist-browser/azure-ai-form-recognizer.js", + format: "umd", + name: "Azure.CognitiveServicesFormRecognizer", + sourcemap: true, + globals: { "@azure/core-http": "Azure.Core.HTTP" } + }, + preserveSymlinks: false, + plugins: [ + sourcemaps(), + replace({ + delimiters: ["", ""], + values: { + // replace dynamic checks with if (false) since this is for + // browser only. Rollup's dead code elimination will remove + // any code guarded by if (isNode) { ... } + "if (isNode)": "if (false)" + } + }), + nodeResolve({ + mainFields: ["module", "browser"], + preferBuiltins: false + }), + cjs({ + namedExports: { + chai: ["assert"], + events: ["EventEmitter"], + "@opentelemetry/types": ["CanonicalCode", "SpanKind", "TraceFlags"] + } + }), + viz({ filename: "dist-browser/browser-stats.html", sourcemap: false }) + ] + }; + + if (test) { + // Entry points - test files under the `test` folder(common for both browser and node), browser specific test files + baseConfig.input = ["dist-esm/test/*.spec.js", "dist-esm/test/browser/*.spec.js"]; + baseConfig.plugins.unshift(multiEntry({ exports: false })); + baseConfig.output.file = "dist-test/index.browser.js"; + + baseConfig.onwarn = (warning) => { + if ( + warning.code === "CIRCULAR_DEPENDENCY" && + warning.importer.indexOf(path.normalize("node_modules/chai/lib") === 0) + ) { + // Chai contains circular references, but they are not fatal and can be ignored. + return; + } + + console.error(`(!) ${warning.message}`); + }; + + // Disable tree-shaking of test code. In rollup-plugin-node-resolve@5.0.0, rollup started respecting + // the "sideEffects" field in package.json. Since our package.json sets "sideEffects=false", this also + // applies to test code, which causes all tests to be removed by tree-shaking. + baseConfig.treeshake = false; + } + + return baseConfig; +} diff --git a/sdk/formrecognizer/ai-form-recognizer/rollup.config.js b/sdk/formrecognizer/ai-form-recognizer/rollup.config.js new file mode 100644 index 000000000000..14652aa67ed8 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/rollup.config.js @@ -0,0 +1,13 @@ +import * as base from "./rollup.base.config"; + +const inputs = []; + +if (!process.env.ONLY_BROWSER) { + inputs.push(base.nodeConfig()); +} + +if (!process.env.ONLY_NODE) { + inputs.push(base.browserConfig()); +} + +export default inputs; diff --git a/sdk/formrecognizer/ai-form-recognizer/rollup.test.config.js b/sdk/formrecognizer/ai-form-recognizer/rollup.test.config.js new file mode 100644 index 000000000000..925a4421a53e --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/rollup.test.config.js @@ -0,0 +1,3 @@ +import * as base from "./rollup.base.config"; + +export default [base.nodeConfig(true), base.browserConfig(true)]; diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/javascript/getLabeledModel.js b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/getLabeledModel.js new file mode 100644 index 000000000000..9b78272373dd --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/getLabeledModel.js @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Get Model + */ + +//import { FormTrainingClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; +const { FormTrainingClient, FormRecognizerApiKeyCredential } = require("../../dist"); + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + console.log(`Running GetModel sample`); + + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const modelId = "afa7d851-ad20-465c-a80f-6ca8cfb879bb"; + + const client = new FormTrainingClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const result = await client.getLabeledModel(modelId, { includeKeys: true }); + console.log(result); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/javascript/getModel.js b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/getModel.js new file mode 100644 index 000000000000..570c31188fbf --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/getModel.js @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Get Model + */ + +const { FormTrainingClient, FormRecognizerApiKeyCredential } = require("../../dist"); +const fs = require("fs"); + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + console.log(`Running GetModel sample`); + + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const modelId = process.env["CUSTOM_FORM_MODEL_ID"] || ""; + + const client = new FormTrainingClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const result = await client.getModel(modelId); + console.dir(result, { depth: 4 }); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/javascript/listModels.js b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/listModels.js new file mode 100644 index 000000000000..e6678f9dc13a --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/listModels.js @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * List Form Recognizer custom models + */ + +//const { FormTrainingClient, FormRecognizerApiKeyCredential } = require("@azure/ai-form-recognizer"); +const { FormTrainingClient, FormRecognizerApiKeyCredential } = require("../../dist"); + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + console.log(`Running listModels sample`); + + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + + const client = new FormTrainingClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + + const result = await client.listModels(); + let i = 0; + for await (const modelInfo of result) { + console.log(`model ${i++}:`); + console.log(modelInfo); + } + + // using `iter.next()` + i = 1; + let iter = client.listModels(); + let modelItem = await iter.next(); + while (!modelItem.done) { + console.log(`model ${i++}: ${modelItem.value.modelId}`); + modelItem = await iter.next(); + } + + // using `byPage()` + i = 1; + for await (const response of client.listModels().byPage()) { + for (const modelInfo of response.modelList) { + console.log(`model ${i++}: ${modelInfo.modelId}`); + } + } +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeContent.js b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeContent.js new file mode 100644 index 000000000000..ec2d591c8bb4 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeContent.js @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Recognize Content + */ + +const { FormRecognizerClient, FormRecognizerApiKeyCredential } = require("../../dist"); +const fs = require("fs"); + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const path = "c:/temp/fw4.pdf"; + + if (!fs.existsSync(path)) { + throw new Error(`Expecting file ${path} exists`); + } + + const readStream = fs.createReadStream(path); + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const poller = await client.beginRecognizeContent(readStream, "application/pdf", { + onProgress: (state) => { console.log(`status: ${state.status}`); } + }); + await poller.pollUntilDone(); + const response = poller.getResult(); + + if (!response) { + throw new Error("Expecting valid response!"); + } + + console.log(response.status); + console.log(response.rawExtractedPages); + console.log(response.extractedPages); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeForm.js b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeForm.js new file mode 100644 index 000000000000..176ce195397d --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeForm.js @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Recognize Forms + */ + +const { FormRecognizerClient, FormRecognizerApiKeyCredential } = require("../../dist"); +const fs = require("fs"); + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const modelId = "8f83f7c3-9666-496b-9335-e7ea5685b5e3"; + const path = "c:/temp/Invoice_6.pdf"; + + const readStream = fs.createReadStream(path); + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const poller = await client.beginRecognizeForms(modelId, readStream, "application/pdf", { + onProgress: (state) => { console.log(`status: ${state.status}`); } + }); + await poller.pollUntilDone(); + const response = poller.getResult(); + + if (!response) { + throw new Error("Expecting valid response!"); + } + + console.log(response.status); + console.log("### Page results:") + for (const page of response.extractedPages || []) { + console.log(`Page number: ${page.page}`); + console.log(`Form type Id: ${page.formTypeId}`); + console.log("key-value pairs"); + for (const field of page.fields || []) { + console.log(`\tkey: ${field.fieldLabel}, value: ${field.valueText}`); + } + console.log("Tables"); + for (const table of page.tables || []) { + for (const row of table.rows) { + for (const cell of row.cells) { + console.log(`cell (${cell.rowIndex},${cell.columnIndex}) ${cell.text}`); + } + } + } + } + + console.log(response.rawExtractedPages); + console.log(response.errors); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeLabeledForm.js b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeLabeledForm.js new file mode 100644 index 000000000000..f161d168cc1c --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeLabeledForm.js @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Recognize Labeled Form + */ + +const { FormRecognizerClient, FormRecognizerApiKeyCredential } = require("../../dist"); +const fs = require("fs"); + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const modelId = "8f83f7c3-9666-496b-9335-e7ea5685b5e3"; // trained with labels + const path = "c:/temp/Invoice_6.pdf"; + + if (!fs.existsSync(path)) { + throw new Error(`Expecting file ${path} exists`); + } + + const readStream = fs.createReadStream(path); + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const poller = await client.beginRecognizeLabeledForms(modelId, readStream, "application/pdf", { + onProgress: (state) => { console.log(`status: ${state.status}`); } + }); + await poller.pollUntilDone(); + const response = poller.getResult(); + + if (!response) { + throw new Error("Expecting valid response!"); + } + + console.log(response.status); + console.log("### Document results:") + for (const document of response.extractedForms || []) { + console.log(`${document.docType}, pages ${document.pageRange}`); + console.log("Fields"); + } + + console.log("### Page results:") + for (const page of response.extractedPages || []) { + console.log(`Page number: ${page.pageNumber}`); + console.log(`Form type Id: ${page.formTypeId}`); + console.log("key-value pairs"); + for (const field of page.fields || []) { + console.log(`\tkey: ${field.fieldLabel}, value: ${field.valueText}`); + } + console.log("Tables"); + for (const table of page.tables || []) { + for (const row of table.rows) { + for (const cell of row.cells) { + console.log(`cell (${cell.rowIndex},${cell.columnIndex}) ${cell.text}`); + } + } + } + } + + console.log(response.rawExtractedPages); + console.log(response.errors); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeLabeledFormFromUrl.js b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeLabeledFormFromUrl.js new file mode 100644 index 000000000000..87b870ee7bc7 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeLabeledFormFromUrl.js @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Recognize Labeled Form from url + */ + +const { FormRecognizerClient, FormRecognizerApiKeyCredential } = require("../../dist"); +const fs = require("fs"); + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + + const modelId = "e28ad0da-aa55-46dc-ade9-839b0d819189"; // trained with labels + const url = process.env["URL_OF_DOCUMENT_TO_ANALYZE_WITH_LABELS"] || ""; + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const poller = await client.beginRecognizeLabeledFormsFromUrl(modelId, url,{ + onProgress: (state) => { console.log(`status: ${state.status}`); } + }); + await poller.pollUntilDone(); + const response = poller.getResult(); + + if (!response) { + throw new Error("Expecting valid response!"); + } + + console.log(response.status); + console.log("### Document results:") + for (const document of response.extractedForms || []) { + console.log(`${document.docType}, pages ${document.pageRange}`); + console.log("Fields"); + } + + console.log("### Page results:") + for (const page of response.extractedPages || []) { + console.log(`Page number: ${page.pageNumber}`); + console.log(`Form type id: ${page.formTypeId}`); + console.log("Fields:"); + for (const field of page.fields || []) { + console.log(`\t${field.fieldLabel.text}: ${field.valueText.text}`); + } + console.log("Tables"); + for (const table of page.tables || []) { + for (const row of table.rows) { + let line = "|"; + for (const cell of row.cells) { + line += `(${cell.rowIndex},${cell.columnIndex}) ${cell.text.padEnd(15)}\t|`; + } + console.log(line); + } + } + } + + console.log("Raw extracted pages:"); + console.log(response.rawExtractedPages); + console.log("Errors:"); + console.log(response.errors); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeReceipt.js b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeReceipt.js new file mode 100644 index 000000000000..451faf8541f4 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeReceipt.js @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Recognize receipt + */ + +const { FormRecognizerClient, FormRecognizerApiKeyCredential } = require("../../dist"); +const fs = require("fs"); + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const path = "c:/temp/contoso-allinone.jpg"; + + if (!fs.existsSync(path)) { + throw new Error(`Expecting file ${path} exists`); + } + + const readStream = fs.createReadStream(path); + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const poller = await client.beginRecognizeReceipts(readStream, "image/jpeg", { + onProgress: (state) => { console.log(`status: ${state.status}`); } + }); + + await poller.pollUntilDone(); + const response = poller.getResult(); + + if (!response) { + throw new Error("Expecting valid response!"); + } + console.log(`### Response status ${response.status}`); + + if (!response.extractedReceipts || response.extractedReceipts.length <= 0) + { + throw new Error("Expecting at lease one receipt in analysis result"); + } + + console.log("### First receipt:") + console.log(response.extractedReceipts[0]); + console.log("### Items:") + console.log("### First receipt:") + console.log(response.extractedReceipts[0]); + console.log("### Items:") + console.table(response.extractedReceipts[0].items, ["name", "quantity", "price", "totalPrice"]); + console.log("### Raw 'MerchantAddress' fields:"); + console.log(response.extractedReceipts[0].fields["MerchantAddress"]) +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeReceiptFromUrl.js b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeReceiptFromUrl.js new file mode 100644 index 000000000000..3306bafaee55 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/recognizeReceiptFromUrl.js @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Recognize receipt from url + */ + +const { FormRecognizerClient, FormRecognizerApiKeyCredential } = require("../../dist"); + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const imageUrl = "https://raw.githubusercontent.com/Azure-Samples/cognitive-services-REST-api-samples/master/curl/form-recognizer/contoso-allinone.jpg"; + + const poller = await client.beginRecognizeReceiptsFromUrl(imageUrl, { + includeTextDetails: true, + onProgress: (state) => { console.log(`analyzing status: ${state.status}`); } + }); + await poller.pollUntilDone(); + const response = poller.getResult(); + + if (!response) { + throw new Error("Expecting valid response!"); + } + console.log(`### Response status ${response.status}`); + + if (!response) { + throw new Error("Expecting analysis result"); + } + + if (!response.extractedReceipts || response.extractedReceipts.length <= 0) + { + throw new Error("Expecting at lease one receipt in analysis result"); + } + + console.log("### First receipt:") + console.log(response.extractedReceipts[0]); + console.log("### Items:") + console.table(response.extractedReceipts[0].items, ["name", "quantity", "price", "totalPrice"]); + + console.log("### Raw 'MerchantAddress' fields:"); + console.log(response.extractedReceipts[0].fields["MerchantAddress"]) +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/javascript/trainModel.js b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/trainModel.js new file mode 100644 index 000000000000..eb81eb11b334 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/trainModel.js @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Train Form Recognizer custom models + */ + +const { FormRecognizerClient, FormRecognizerApiKeyCredential } = require("../../dist"); + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + console.log(`Running TrainModel sample`); + + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + + const trainingDataSource = process.env["DOCUMENT_SOURCE"] || ""; + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const trainingClient = client.getFormTrainingClient(); + + const poller = await trainingClient.beginTraining(trainingDataSource, { + onProgress: (state) => { console.log(`training status: ${state.status}`); } + }); + await poller.pollUntilDone(); + const model = poller.getResult(); + console.dir(model, { depth: 4 }); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/javascript/trainModelWithLabels.js b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/trainModelWithLabels.js new file mode 100644 index 000000000000..b7d1f309d008 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/javascript/trainModelWithLabels.js @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Train Form Recognizer custom models + */ + +// const { FormRecognizerClient, FormRecognizerApiKeyCredential } = require("@azure/ai-form-recognizer"); +const { FormRecognizerClient, FormRecognizerApiKeyCredential } = require("../../dist"); + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + console.log(`Running TrainModel sample`); + + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const trainingDataSource = process.env["LABELED_DOCUMENT_SOURCE"] || ""; + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const trainingClient = client.getFormTrainingClient(); + + const poller = await trainingClient.beginTrainingWithLabel(trainingDataSource, { + onProgress: (state) => { console.log(`training status: ${state.status}`); } + }); + await poller.pollUntilDone(); + const model = poller.getResult(); + console.dir(model, { depth: 4 }); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/tsconfig.json b/sdk/formrecognizer/ai-form-recognizer/samples/tsconfig.json new file mode 100644 index 000000000000..8c89eac7173a --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "typescript/dist", + "lib": ["DOM", "ES6"] + }, + "include": ["typescript/src/**.ts"], + "exclude": ["typescript/*.json", "**/node_modules/", "../node_modules", "../typings"] +} diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/typescript/package.json b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/package.json new file mode 100644 index 000000000000..d1ae0ad1ce3f --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/package.json @@ -0,0 +1,41 @@ +{ + "name": "azure-ai-form-recognizer-samples-ts", + "private": true, + "version": "0.1.0", + "description": "Azure Cognitive Services Form Recognizer client library samples for TypeScript", + "engine": { + "node": ">=8.0.0" + }, + "scripts": { + "build": "tsc", + "prebuild": "rimraf dist/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Azure/azure-sdk-for-js.git" + }, + "keywords": [ + "Azure", + "Cognitive Services", + "Form Recognizer", + "Node.js", + "TypeScript" + ], + "author": "Microsoft Corporation", + "license": "MIT", + "bugs": { + "url": "https://github.com/Azure/azure-sdk-for-js/issues" + }, + "homepage": "https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/formrecognizer/ai-form-recognizer", + "sideEffects": false, + "dependencies": { + "@azure/ai-ai-form-recognizer": "latest", + "@azure/identity": "latest", + "dotenv": "^8.2.0" + }, + "devDependencies": { + "@types/node": "^8.0.0", + "rimraf": "^3.0.0", + "typescript": "~3.6.4" + } +} diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/getLabeledModel.ts b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/getLabeledModel.ts new file mode 100644 index 000000000000..902bcd1c1599 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/getLabeledModel.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Get Model + */ + +//import { FormTrainingClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; +import { FormTrainingClient, FormRecognizerApiKeyCredential } from "../../../src/index"; + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + console.log(`Running GetModel sample`); + + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const modelId = "cbfd7961-99c1-49ca-8974-2fa0c9f54508"; + + const client = new FormTrainingClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const result = await client.getLabeledModel(modelId, { includeKeys: true }); + console.dir(result, { depth: 4 }); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/getModel.ts b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/getModel.ts new file mode 100644 index 000000000000..5fab504b71e3 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/getModel.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Get Model + */ + +//import { FormTrainingClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; +import { FormTrainingClient, FormRecognizerApiKeyCredential } from "../../../src/index"; + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + console.log(`Running GetModel sample`); + + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const modelId = process.env["CUSTOM_FORM_MODEL_ID"] || ""; + + const client = new FormTrainingClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const result = await client.getModel(modelId); + console.dir(result, { depth: 4 }); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/listModels.ts b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/listModels.ts new file mode 100644 index 000000000000..9632d11c05b1 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/listModels.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * List Form Recognizer custom models + */ + +//import { FormTrainingClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; +import { FormTrainingClient, FormRecognizerApiKeyCredential } from "../../../src/index"; + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + console.log(`Running listModels sample`); + + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + + const client = new FormTrainingClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + + // using `for await` syntax: + const result = client.listModels(); + let i = 0; + for await (const model of result) { + console.log(`model ${i++}:`); + console.log(model); + } + + // using `iter.next()` + i = 1; + let iter = client.listModels(); + let modelItem = await iter.next(); + while (!modelItem.done) { + console.log(`model ${i++}: ${modelItem.value.modelId}`); + modelItem = await iter.next(); + } + + // using `byPage()` + i = 1; + for await (const response of client.listModels().byPage()) { + for (const modelInfo of response.modelList!) { + console.log(`model ${i++}: ${modelInfo.modelId}`); + } + } +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeContent.ts b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeContent.ts new file mode 100644 index 000000000000..c4957fc28285 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeContent.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Recognize Layout + */ + +//import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; +import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "../../../src/index"; +import * as fs from "fs"; + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const path = "c:/temp/fw4.pdf"; + + if (!fs.existsSync(path)) { + throw new Error(`Expecting file ${path} exists`); + } + + const readStream = fs.createReadStream(path); + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const poller = await client.beginRecognizeContent(readStream, "application/pdf", { + onProgress: (state) => { console.log(`status: ${state.status}`); } + }); + await poller.pollUntilDone(); + const response = poller.getResult(); + + if (!response) { + throw new Error("Expecting valid response!"); + } + + console.log(response.status); + console.log(response.rawExtractedPages); + console.log(response.extractedLayoutPages); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeForm.ts b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeForm.ts new file mode 100644 index 000000000000..11ce3f131491 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeForm.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Recognize Forms + */ + +//import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; +import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "../../../src/index"; +import * as fs from "fs"; + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const modelId = "8f83f7c3-9666-496b-9335-e7ea5685b5e3"; + const path = "c:/temp/Invoice_6.pdf"; + + if (!fs.existsSync(path)) { + throw new Error(`Expecting file ${path} exists`); + } + + const readStream = fs.createReadStream(path); + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const poller = await client.beginRecognizeForms(modelId, readStream, "application/pdf", { + onProgress: (state) => { console.log(`status: ${state.status}`); } + }); + await poller.pollUntilDone(); + const response = poller.getResult(); + + if (!response) { + throw new Error("Expecting valid response!"); + } + + console.log(response.status); + console.log("### Page results:") + for (const page of response.extractedPages || []) { + console.log(`Page number: ${page.pageNumber}`); + console.log(`Form type id: ${page.formTypeId}`); + console.log("key-value pairs"); + for (const field of page.fields || []) { + console.log(`\tkey: ${field.fieldLabel}, value: ${field.valueText}`); + } + console.log("Tables"); + for (const table of page.tables || []) { + for (const row of table.rows) { + for (const cell of row.cells) { + console.log(`cell (${cell.rowIndex},${cell.columnIndex}) ${cell.text}`); + } + } + } + } + + console.log(response.rawExtractedPages); + console.log(response.errors); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeLabeledForm.ts b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeLabeledForm.ts new file mode 100644 index 000000000000..811676cefc5c --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeLabeledForm.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Recognize Labeled Form + */ + +//import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; +import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "../../../src/index"; +import * as fs from "fs"; + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const modelId = "afa7d851-ad20-465c-a80f-6ca8cfb879bb"; // trained with labels + const path = "c:/temp/Invoice_6.pdf"; + + if (!fs.existsSync(path)) { + throw new Error(`Expecting file ${path} exists`); + } + + const readStream = fs.createReadStream(path); + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const poller = await client.beginRecognizeLabeledForms(modelId, readStream, "application/pdf", { + onProgress: (state) => { console.log(`status: ${state.status}`); } + }); + await poller.pollUntilDone(); + const response = poller.getResult(); + + if (!response) { + throw new Error("Expecting valid response!"); + } + + console.log(response.status); + console.log("### Document results:") + for (const document of response.extractedForms || []) { + console.log(`${document.docType}, pages ${document.pageRange}`); + console.log("Fields"); + } + + console.log("### Page results:") + for (const page of response.extractedPages || []) { + console.log(`Page number: ${page.pageNumber}`); + console.log(`Form type id: ${page.formTypeId}`); + console.log("key-value pairs"); + for (const field of page.fields || []) { + console.log(`\tkey: ${field.fieldLabel}, value: ${field.valueText}`); + } + console.log("Tables"); + for (const table of page.tables || []) { + for (const row of table.rows) { + for (const cell of row.cells) { + console.log(`cell (${cell.rowIndex},${cell.columnIndex}) ${cell.text}`); + } + } + } + } + + console.log(response.rawExtractedPages); + console.log(response.errors); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeLabeledFormFromUrl.ts b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeLabeledFormFromUrl.ts new file mode 100644 index 000000000000..75c83dff0064 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeLabeledFormFromUrl.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Recognize Labeled Form from url + */ + +//import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; +import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "../../../src/index"; + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + + const modelId = "e28ad0da-aa55-46dc-ade9-839b0d819189"; // trained with labels + const url = process.env["URL_OF_DOCUMENT_TO_ANALYZE_WITH_LABELS"] || ""; + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const poller = await client.beginRecognizeLabeledFormsFromUrl(modelId, url,{ + onProgress: (state) => { console.log(`status: ${state.status}`); } + }); + await poller.pollUntilDone(); + const response = poller.getResult(); + + if (!response) { + throw new Error("Expecting valid response!"); + } + + console.log(response.status); + console.log("### Document results:") + for (const document of response.extractedForms || []) { + console.log(`${document.docType}, pages ${document.pageRange}`); + console.log("Fields"); + } + + console.log("### Page results:") + for (const page of response.extractedPages || []) { + console.log(`Page number: ${page.pageNumber}`); + console.log(`Form type id: ${page.formTypeId}`); + console.log("key-value pairs"); + for (const field of page.fields || []) { + console.log(`\t${field.fieldLabel.text}: ${field.valueText.text}`); + } + console.log("Tables"); + for (const table of page.tables || []) { + for (const row of table.rows) { + for (const cell of row.cells) { + console.log(`cell (${cell.rowIndex},${cell.columnIndex}) ${cell.text}`); + } + } + } + } + + console.log("Raw extracted pages:"); + console.log(response.rawExtractedPages); + console.log("Errors:"); + console.log(response.errors); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeReceipt.ts b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeReceipt.ts new file mode 100644 index 000000000000..f871df0ab8cb --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeReceipt.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Recognize receipt + */ + +//import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; +import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "../../../src/index"; + +import * as fs from "fs"; + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const path = "c:/temp/contoso-allinone.jpg"; + + if (!fs.existsSync(path)) { + throw new Error(`Expecting file ${path} exists`); + } + + const readStream = fs.createReadStream(path); + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const poller = await client.beginRecognizeReceipts(readStream, "image/jpeg", { + onProgress: (state) => { console.log(`status: ${state.status}`); } + }); + + await poller.pollUntilDone(); + const response = poller.getResult(); + + if (!response) { + throw new Error("Expecting valid response!"); + } + console.log(`### Response status ${response.status}`); + + if (!response.extractedReceipts || response.extractedReceipts.length <= 0) + { + throw new Error("Expecting at lease one receipt in analysis result"); + } + + console.log("### First receipt:") + console.log(response.extractedReceipts[0]); + console.log("### Items:") + console.table(response.extractedReceipts[0].items, ["name", "quantity", "price", "totalPrice"]); + console.log("### Raw 'MerchantAddress' fields:"); + console.log(response.extractedReceipts[0]?.fields["MerchantAddress"]) +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeReceiptFromUrl.ts b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeReceiptFromUrl.ts new file mode 100644 index 000000000000..302ae370760b --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/recognizeReceiptFromUrl.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Recognize receipt from url + */ + +//import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; +import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "../../../src/index"; + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const imageUrl = "https://raw.githubusercontent.com/Azure-Samples/cognitive-services-REST-api-samples/master/curl/form-recognizer/contoso-allinone.jpg"; + + const poller = await client.beginRecognizeReceiptsFromUrl( + imageUrl, { + includeTextDetails: true, + onProgress: (state) => { console.log(`analyzing status: ${state.status}`); } + }); + await poller.pollUntilDone(); + const response = poller.getResult(); + + if (!response) { + throw new Error("Expecting valid response!"); + } + console.log(`### Response status ${response.status}`); + + if (!response.extractedReceipts || response.extractedReceipts.length <= 0) + { + throw new Error("Expecting at lease one receipt in analysis result"); + } + + console.log("### First receipt:") + console.log(response.extractedReceipts[0]); + console.log("### Items:") + console.log("### First receipt:") + console.log(response.extractedReceipts[0]); + console.log("### Items:") + console.table(response.extractedReceipts?[0].items, ["name", "quantity", "price", "totalPrice"]); + console.log("### Raw 'MerchantAddress' fields:"); + console.log(response.extractedReceipts[0]?.fields["MerchantAddress"]) +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/trainModel.ts b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/trainModel.ts new file mode 100644 index 000000000000..2abff69d1558 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/trainModel.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Train Form Recognizer custom models + */ + +//import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; +import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "../../../src/index"; + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + console.log(`Running TrainModel sample`); + + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const trainingDataSource = process.env["DOCUMENT_SOURCE"] || ""; + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const trainingClient = client.getFormTrainingClient(); + + const poller = await trainingClient.beginTraining(trainingDataSource, { + onProgress: (state) => { console.log("training status: "); console.log(state); } + }); + await poller.pollUntilDone(); + const model = poller.getResult(); + console.dir(model, { depth: 4 }); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/trainModelWithLabels.ts b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/trainModelWithLabels.ts new file mode 100644 index 000000000000..2251e8bfe240 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/samples/typescript/src/trainModelWithLabels.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Train Form Recognizer custom models + */ + +//import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; +import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "../../../src/index"; + +// Load the .env file if it exists +require("dotenv").config(); + +async function main() { + console.log(`Running TrainModel sample`); + + // You will need to set these environment variables or edit the following values + const endpoint = process.env["COGNITIVE_SERVICE_ENDPOINT"] || ""; + const apiKey = process.env["COGNITIVE_SERVICE_API_KEY"] || ""; + const trainingDataSource = process.env["LABELED_DOCUMENT_SOURCE"] || ""; + + const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + const trainingClient = client.getFormTrainingClient(); + + const poller = await trainingClient.beginTrainingWithLabel(trainingDataSource, { + onProgress: (state) => { console.log("training status: "); console.log(state.status); } + }); + await poller.pollUntilDone(); + const model = poller.getResult(); + console.dir(model, { depth: 4 }); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/src/common.ts b/sdk/formrecognizer/ai-form-recognizer/src/common.ts new file mode 100644 index 000000000000..bc500d2f1865 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/common.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { PipelineOptions, OperationOptions, HttpRequestBody } from "@azure/core-http"; +import { FormRecognizerRequestBody } from "./models"; +import { ContentType, SourcePath } from "./generated/models"; +import { streamToBuffer } from "./utils/utils.node"; +import { MAX_INPUT_DOCUMENT_SIZE } from './constants'; + +/** + * Client options used to configure Form Recognizer API requests. + */ +export interface FormRecognizerClientOptions extends PipelineOptions {} + +/** + * Options common to all form recognizer operations. + */ +export interface FormRecognizerOperationOptions extends OperationOptions {} + +/** + * Translate the content to a format that is understood by Form Recognizer service + * @internal + */ +export async function toRequestBody( + body: FormRecognizerRequestBody +): Promise { + if (typeof body === "string") { + return { + source: body + }; + } else { + // cache stream to allow retry + if (isReadableStream(body)) { + return await streamToBuffer(body, MAX_INPUT_DOCUMENT_SIZE); + } + + return body as HttpRequestBody; + } +} + +function isReadableStream(data: FormRecognizerRequestBody): data is NodeJS.ReadableStream { + return "read" in data && typeof data.read === "function"; +} + +function isBlob(data: FormRecognizerRequestBody): data is Blob { + return "size" in data && "type" in data; +} + +function isArrayBuffer(data: FormRecognizerRequestBody): data is ArrayBuffer { + return "byteLength" in data && "slice" in data && typeof data.slice === "function"; +} + +function isArrayBufferView(data: FormRecognizerRequestBody): data is ArrayBufferView { + return "buffer" in data && "byteLength" in data && "byteOffset" in data; +} + +function isSourcePath(data: FormRecognizerRequestBody | SourcePath): data is SourcePath { + return "source" in data; +} + +export async function getContentType( + data: Blob | ArrayBuffer | ArrayBufferView | SourcePath +): Promise { + if (isSourcePath(data)) { + return undefined; + } + let bytes: Uint8Array; + if (isArrayBuffer(data)) { + // ArrayBuffer + if (data.byteLength < 4) { + throw new RangeError("Invalid input. Expect more than 4 bytes of data"); + } + + bytes = new Uint8Array(data, 0, 4); + } else if (isArrayBufferView(data)) { + // ArrayBufferView + if (data.byteLength < 4) { + throw new RangeError("Invalid input. Expect more than 4 bytes of data"); + } + + bytes = new Uint8Array(data.buffer, 0, 4); + } else if (isBlob(data)) { + // Blob + const arrayPromise = new Promise(function(resolve) { + var reader = new FileReader(); + + reader.onloadend = function() { + resolve(reader.result as ArrayBuffer); + }; + + reader.readAsArrayBuffer(data); + }); + + const buffer = await arrayPromise; + if (buffer.byteLength < 4) { + throw new RangeError("Invalid input. Expect more than 4 bytes of data"); + } + + bytes = new Uint8Array(buffer, 0, 4); + } else { + throw new Error("unsupported request body type"); + } + + if (bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46) { + return "application/pdf"; + } else if (bytes[0] === 0xff && bytes[1] === 0xd8) { + return "image/jpeg"; + } else if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) { + return "image/png"; + } else if ( + (bytes[0] === 0x49 && bytes[1] === 0x49 && bytes[2] === 0x2a && bytes[3] === 0x0) || + (bytes[0] === 0x4d && bytes[1] === 0x4d && bytes[2] === 0x0 && bytes[3] === 0x2a) + ) { + return "image/tiff"; + } else { + throw new RangeError("content type could not be detected"); + } +} diff --git a/sdk/formrecognizer/ai-form-recognizer/src/constants.ts b/sdk/formrecognizer/ai-form-recognizer/src/constants.ts new file mode 100644 index 000000000000..cf3a1f74dea6 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/constants.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export const SDK_VERSION: string = "1.0.0-preview.1"; + +export const DEFAULT_COGNITIVE_SCOPE = "https://cognitiveservices.azure.com/.default"; + +export const LIB_INFO = `azsdk-js-ai-formrecognizer/${SDK_VERSION}`; + +export const MAX_INPUT_DOCUMENT_SIZE = 50 * 1024 * 1024; // 50 MB diff --git a/sdk/formrecognizer/ai-form-recognizer/src/formRecognizerApiKeyCredential.ts b/sdk/formrecognizer/ai-form-recognizer/src/formRecognizerApiKeyCredential.ts new file mode 100644 index 000000000000..e321b9827cc9 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/formRecognizerApiKeyCredential.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { ServiceClientCredentials, WebResource } from "@azure/core-http"; + +const API_KEY_HEADER_NAME = "Ocp-Apim-Subscription-Key"; + +/** + * This class is used to authenticate to Form Recognizer using an API Key retrieved + * from the Azure portal. Sometimes this is referred to as a "Subscription Key". + */ +export class FormRecognizerApiKeyCredential implements ServiceClientCredentials { + /** + * Used to authenticate to Form Recognizer service + */ + private apiKey!: string; + + /** + * Creates a new apiKeyCredential object. + * + * @param {string} apiKey The Form Recognizer API key for authentication. + */ + constructor(apiKey: string) { + this.updateKey(apiKey); + } + + /** + * Updates the API Key used for authentication. Use this method to roll credentials. + * @param apiKey The Form Recognizer API key for authentication. + */ + public updateKey(apiKey: string): void { + if (!apiKey || typeof apiKey !== "string") { + throw new Error("apiKey must be a non-empty string"); + } + + this.apiKey = apiKey; + } + + /* eslint-disable @azure/azure-sdk/ts-use-interface-parameters */ + /** + * Signs a request with the provided API Key. + * + * @param {WebResource} webResource The WebResource to be signed. + * @returns {Promise} The signed request object. + */ + public async signRequest(webResource: WebResource): Promise { + if (!webResource) { + throw new Error("webResource cannot be null or undefined."); + } + + webResource.headers.set(API_KEY_HEADER_NAME, this.apiKey); + return webResource; + } +} +/* eslint-enable @azure/azure-sdk/ts-use-interface-parameters */ diff --git a/sdk/formrecognizer/ai-form-recognizer/src/formRecognizerClient.ts b/sdk/formrecognizer/ai-form-recognizer/src/formRecognizerClient.ts new file mode 100644 index 000000000000..761b8b71dd17 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/formRecognizerClient.ts @@ -0,0 +1,794 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + createPipelineFromOptions, + signingPolicy, + InternalPipelineOptions, + operationOptionsToRequestOptionsBase, + AbortSignalLike +} from "@azure/core-http"; +import { SDK_VERSION } from "./constants"; +import { logger } from "./logger"; +import { createSpan } from "./tracing"; +import { + FormRecognizerClientOptions, + FormRecognizerOperationOptions, + toRequestBody, + getContentType +} from "./common"; +import { CanonicalCode } from "@opentelemetry/types"; + +import { FormRecognizerClient as GeneratedClient } from "./generated/formRecognizerClient"; +import { + FormRecognizerClientAnalyzeWithCustomModelResponse as AnalyzeWithCustomModelResponseModel, + FormRecognizerClientAnalyzeLayoutAsyncResponse as AnalyzeLayoutAsyncResponseModel, + FormRecognizerClientAnalyzeReceiptAsyncResponse as AnalyzeReceiptAsyncResponseModel, + ContentType, +} from "./generated/models"; +import { FormRecognizerApiKeyCredential } from "./formRecognizerApiKeyCredential"; +import { PollOperationState, PollerLike } from "@azure/core-lro"; +import { + RecognizePollerClient, + BeginRecognizePoller, + BeginRecognizePollState, + RecognizeOptions +} from "./lro/analyze/poller"; +import { + RecognizeContentResultResponse, + RecognizeFormResultResponse, + LabeledFormResultResponse, + RecognizeReceiptResultResponse, + FormRecognizerRequestBody +} from "./models"; +import { + toRecognizeFormResultResponse, + toLabeledFormResultResponse, + toAnalyzeLayoutResultResponse, + toReceiptResultResponse +} from "./transforms"; +import { FormTrainingClient } from "./formTrainingClient"; + +export { + ContentType +}; + +export { PollOperationState, PollerLike }; + +/** + * Options for analyzing layout + */ +export type RecognizeContentOptions = FormRecognizerOperationOptions; + +/** + * Options for the start analyzing layout operation + */ +export type BeginRecognizeContentOptions = RecognizeContentOptions & { + /** + * Delay to wait until next poll, in milliseconds + */ + intervalInMs?: number; + /** + * Callback to progress events triggered in the Recognize Content Long-Running-Operation (LRO) + */ + onProgress?: (state: BeginRecognizePollState) => void; + /** + * A serialized poller which can be used to resume an existing paused Long-Running-Operation. + */ + resumeFrom?: string; +}; + +/** + * The Long-Running-Operation (LRO) poller that allows you to wait until form content is recognized. + */ +export type ContentPollerLike = PollerLike< + PollOperationState, + RecognizeContentResultResponse +>; + +/** + * Options for retrieving recognized content data + */ +type GetRecognizedContentResultOptions = FormRecognizerOperationOptions; + +/** + * Options for recognition of forms + */ +export type RecognizeFormsOptions = FormRecognizerOperationOptions & { + /** + * Specifies whether to include text lines and element references in the result + */ + includeTextDetails?: boolean; +}; + +/** + * Options for starting analyzing form operation + */ +export type BeginRecognizeFormsOptions = RecognizeFormsOptions & { + /** + * Delay to wait until next poll, in milliseconds + */ + intervalInMs?: number; + /** + * Callback to progress events triggered in the Recognize Form Long-Running-Operation (LRO) + */ + onProgress?: (state: BeginRecognizePollState) => void; + /** + * A serialized poller which can be used to resume an existing paused Long-Running-Operation. + */ + resumeFrom?: string; +}; + +/** + * Options for starting analyzing form operation + */ +export type BeginRecognizeLabeledFormOptions = RecognizeFormsOptions & { + /** + * Delay to wait until next poll, in milliseconds + */ + intervalInMs?: number; + /** + * Callback to progress events triggered in the Recognize Labeled Form Long-Running-Operation (LRO) + */ + onProgress?: (state: BeginRecognizePollState) => void; + /** + * A serialized poller which can be used to resume an existing paused Long-Running-Operation. + */ + resumeFrom?: string; +}; + +/** + * Result type of the Recognize Form Long-Running-Operation (LRO) + */ +export type FormPollerLike = PollerLike< + PollOperationState, + RecognizeFormResultResponse +>; + +/** + * Result of the Recognize Labeled Form Long-Running-Operation (LRO) + */ +export type LabeledFormPollerLike = PollerLike< + PollOperationState, + LabeledFormResultResponse +>; + +/** + * Options for retrieving result of Recognize Form operation + */ +type GetRecognizedFormsOptions = FormRecognizerOperationOptions; + +/** + * Options for Recognize Receipt operation + */ +export type RecognizeReceiptsOptions = FormRecognizerOperationOptions & { + /** + * Specifies whether to include text lines and element references in the result + */ + includeTextDetails?: boolean; +}; + +/** + * Options for retrieving recognized receipt data + */ +type GetRecognizedReceiptsOptions = FormRecognizerOperationOptions; + +/** + * Options for Begin Analyze Receipt operation + */ +export type BeginRecognizeReceiptsOptions = RecognizeReceiptsOptions & { + /** + * Delay to wait until next poll, in milliseconds + */ + intervalInMs?: number; + /** + * Callback to progress events triggered in the Recognize Receipt Long-Running-Operation (LRO) + */ + onProgress?: (state: BeginRecognizePollState) => void; + /** + * A serialized poller which can be used to resume an existing paused Long-Running-Operation. + */ + resumeFrom?: string; +}; + +/** + * The Long-Running-Operation (LRO) poller that allows you to wait until receipt(s) are recognized. + */ +export type ReceiptPollerLike = PollerLike< + PollOperationState, + RecognizeReceiptResultResponse +>; + +/** + * Client class for interacting with Azure Form Recognizer. + */ +export class FormRecognizerClient { + /** + * The URL to Azure Form Recognizer service endpoint + */ + public readonly endpointUrl: string; + + /** + * @internal + * @ignore + */ + private readonly credential: FormRecognizerApiKeyCredential; + + /** + * @internal + * @ignore + * A reference to the auto-generated FormRecognizer HTTP client. + */ + private readonly client: GeneratedClient; + + /** + * Creates an instance of FormRecognizerClient. + * + * Example usage: + * ```ts + * import { FormRecognizerClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; + * + * const client = new FormRecognizerClient( + * "", + * new FormRecognizerApiKeyCredential("") + * ); + * ``` + * @param {string} endpointUrl The URL to Azure Form Recognizer service endpoint + * @param {FormRecognizerApiKeyCredential} credential Used to authenticate requests to the service. + * @param {FormRecognizerClientOptions} [options] Used to configure the FormRecognizer client. + */ + constructor( + endpointUrl: string, + credential: FormRecognizerApiKeyCredential, + options: FormRecognizerClientOptions = {} + ) { + this.endpointUrl = endpointUrl; + this.credential = credential; + const { ...pipelineOptions } = options; + + const libInfo = `azsdk-js-ai-formrecognizer/${SDK_VERSION}`; + if (!pipelineOptions.userAgentOptions) { + pipelineOptions.userAgentOptions = {}; + } + if (pipelineOptions.userAgentOptions.userAgentPrefix) { + pipelineOptions.userAgentOptions.userAgentPrefix = `${pipelineOptions.userAgentOptions.userAgentPrefix} ${libInfo}`; + } else { + pipelineOptions.userAgentOptions.userAgentPrefix = libInfo; + } + + const authPolicy = signingPolicy(credential); + + const internalPipelineOptions: InternalPipelineOptions = { + ...pipelineOptions, + ...{ + loggingOptions: { + logger: logger.info, + allowedHeaderNames: ["x-ms-correlation-request-id", "x-ms-request-id"] + } + } + }; + + const pipeline = createPipelineFromOptions(internalPipelineOptions, authPolicy); + this.client = new GeneratedClient(credential, this.endpointUrl, pipeline); + } + + /** + * Creates an instance of {@link FormTrainingClient}. + */ + public getFormTrainingClient(): FormTrainingClient { + return new FormTrainingClient(this.endpointUrl, this.credential); + } + + /** + * Recognizes content, including text and table structure from documents. + * + * This method returns a long running operation poller that allows you to wait + * indefinitely until the copy is completed. + * You can also cancel a copy before it is completed by calling `cancelOperation` on the poller. + * Note that the onProgress callback will not be invoked if the operation completes in the first + * request, and attempting to cancel a completed copy will result in an error being thrown. + * + * Example usage: + * ```ts + * const path = "./fw4.pdf"; + * const readStream = fs.createReadStream(path); + * + * const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + * const poller = await client.beginRecognizeContent(readStream, "application/pdf", { + * onProgress: (state) => { console.log(`status: ${state.status}`); } + * }); + * + * await poller.pollUntilDone(); + * const response = poller.getResult(); + + * console.log(response.status); + * console.log(response.rawExtractedPages); + * console.log(response.extractedLayoutPages); + * ``` + * @summary Recognizes receipt information from a given document + * @param {FormRecognizerRequestBody} source Input document + * @param {contentType} Content type of the input. Supported types are "application/pdf", "image/jpeg", "image/png", and "image/tiff"; + * @param {BeginRecognizeContentOptions} [options] Options to the Begin Recognize Content operation + */ + public async beginRecognizeContent( + source: FormRecognizerRequestBody, + contentType?: ContentType, + options: BeginRecognizeContentOptions = {} + ): Promise { + const analyzePollerClient: RecognizePollerClient = { + beginRecognize: (...args) => recognizeLayoutInternal(this.client, ...args), + getRecognizeResult: (...args) => this.getRecognizedContent(...args) + }; + + const poller = new BeginRecognizePoller({ + client: analyzePollerClient, + source, + contentType, + ...options + }); + + await poller.poll(); + return poller; + } + + public async beginRecognizeContentFromUrl( + documentUrl: string, + options: BeginRecognizeContentOptions = {} + ): Promise { + return this.beginRecognizeContent(documentUrl, undefined, options); + } + + /** + * @private + */ + private async getRecognizedContent(resultId: string, options?: GetRecognizedContentResultOptions) { + const realOptions = options || {}; + const { span, updatedOptions: finalOptions } = createSpan( + "FormRecognizerClient-getRecognizedLayoutResult", + realOptions + ); + + try { + const requestOptions = operationOptionsToRequestOptionsBase(finalOptions); + const analyzeResult = await this.client.getAnalyzeLayoutResult(resultId, requestOptions); + return toAnalyzeLayoutResultResponse(analyzeResult); + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } + /** + * Recognizes name-value pairs and tables from a given document using a model from unsupervised training. + * This method returns a long running operation poller that allows you to wait + * indefinitely until the copy is completed. + * You can also cancel a copy before it is completed by calling `cancelOperation` on the poller. + * Note that the onProgress callback will not be invoked if the operation completes in the first + * request, and attempting to cancel a completed copy will result in an error being thrown. + * + * Example usage: + * ```ts + * const path = "./Invoice_6.pdf"; + * const readStream = fs.createReadStream(path); + * + * const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + * const poller = await client.beginRecognizeForms(modelId, readStream, "application/pdf", { + * onProgress: (state) => { console.log(`status: ${state.status}`); } + * }); + * await poller.pollUntilDone(); + * const response = poller.getResult(); + * console.log(response.status); + * ``` + * @summary Recognizes form information from a given document using unlabeled model. + * @param {string} modelId Id of the model to use + * @param {FormRecognizerRequestBody} body Input document + * @param {contentType} Content type of the input. Supported types are "application/pdf", "image/jpeg", "image/png", and "image/tiff"; + * @param {BeginRecognizeFormsOptions} [options] Options to the BeginRecognizeForms operation + */ + public async beginRecognizeForms( + modelId: string, + body: FormRecognizerRequestBody, + contentType?: ContentType, + options: BeginRecognizeFormsOptions = {} + ): Promise { + if (!modelId) { + throw new RangeError("Invalid model id"); + } + const analyzePollerClient: RecognizePollerClient = { + beginRecognize: ( + body: FormRecognizerRequestBody, + contentType?: ContentType, + analyzeOptions: RecognizeOptions = {}, + modelId?: string + ) => recognizeCustomFormInternal(this.client, body, contentType, analyzeOptions, modelId!), + getRecognizeResult: (resultId: string, options: { abortSignal?: AbortSignalLike }) => + this.getRecognizedForm(modelId, resultId, options) + }; + + const poller = new BeginRecognizePoller({ + client: analyzePollerClient, + modelId, + source: body, + contentType, + ...options + }); + + await poller.poll(); + return poller; + } + + public async beginRecognizeFormsFromUrl( + modelId: string, + documentUrl: string, + options: BeginRecognizeFormsOptions = {} + ): Promise, RecognizeFormResultResponse>> { + if (!modelId) { + throw new RangeError("Invalid modelId"); + } + return this.beginRecognizeForms(modelId, documentUrl, undefined, options); + } + + private async getRecognizedForm( + modelId: string, + resultId: string, + options?: GetRecognizedFormsOptions + ): Promise { + const realOptions = options || {}; + const { span, updatedOptions: finalOptions } = createSpan( + "FormRecognizerClient-getRecognizedForm", + realOptions + ); + + try { + const result = await this.client.getAnalyzeFormResult( + modelId, + resultId, + operationOptionsToRequestOptionsBase(finalOptions) + ); + return toRecognizeFormResultResponse(result); + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } + + private async getRecognizedLabeledForms( + modelId: string, + resultId: string, + options?: GetRecognizedFormsOptions + ): Promise { + const realOptions = options || {}; + const { span, updatedOptions: finalOptions } = createSpan( + "FormRecognizerClient-getRecognizedLabeledForm", + realOptions + ); + + try { + const result = await this.client.getAnalyzeFormResult( + modelId, + resultId, + operationOptionsToRequestOptionsBase(finalOptions) + ); + return toLabeledFormResultResponse(result); + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } + + /** + * Recognizes name-value pairs and tables from a given document using a model from supervised training with labels. + * This method returns a long running operation poller that allows you to wait + * indefinitely until the copy is completed. + * You can also cancel a copy before it is completed by calling `cancelOperation` on the poller. + * Note that the onProgress callback will not be invoked if the operation completes in the first + * request, and attempting to cancel a completed copy will result in an error being thrown. + * + * Example usage: + * ```ts + * const path = "./Invoice_6.pdf"; + * const readStream = fs.createReadStream(path); + * + * const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + * const poller = await client.beginRecognizeLabeledForms(modelId, readStream, "application/pdf", { + * onProgress: (state) => { console.log(`status: ${state.status}`); } + * }); + * await poller.pollUntilDone(); + * const response = poller.getResult(); + * console.log(response.status); + * ``` + * @summary Recognizes form information from a given document using labeled model. + * @param {string} modelId Id of the model to use + * @param {FormRecognizerRequestBody} body Input document + * @param {contentType} Content type of the input. Supported types are "application/pdf", "image/jpeg", "image/png", and "image/tiff"; + * @param {BeginRecognizeLabeledFormsOptions} [options] Options to the BeginRecognizeLabeledForms operation + */ + public async beginRecognizeLabeledForms( + modelId: string, + body: FormRecognizerRequestBody, + contentType: ContentType, + options: BeginRecognizeLabeledFormOptions = {} + ): Promise { + if (!modelId) { + throw new RangeError("Invalid model id"); + } + const analyzePollerClient: RecognizePollerClient = { + beginRecognize: ( + body: FormRecognizerRequestBody, + contentType?: ContentType, + analyzeOptions?: RecognizeOptions, + modelId?: string + ) => recognizeCustomFormInternal(this.client, body, contentType, analyzeOptions, modelId!), + getRecognizeResult: (resultId: string, options: { abortSignal?: AbortSignalLike }) => + this.getRecognizedLabeledForms(modelId, resultId, options) + }; + + const poller = new BeginRecognizePoller({ + client: analyzePollerClient, + modelId, + source: body, + contentType, + ...options + }); + + await poller.poll(); + return poller; + } + + public async beginRecognizeLabeledFormsFromUrl( + modelId: string, + documentUrl: string, + options: BeginRecognizeLabeledFormOptions = {} + ): Promise, LabeledFormResultResponse>> { + if (!modelId) { + throw new RangeError("Invalid model id"); + } + + return this.beginRecognizeForms(modelId, documentUrl, undefined, options); + } + + /** + * Recognizes data from receipts using pre-built receipt model, enabling you to extract structure data + * from receipts such as merchant name, merchant phone number, transaction date, and more. + * + * This method returns a long running operation poller that allows you to wait + * indefinitely until the copy is completed. + * You can also cancel a copy before it is completed by calling `cancelOperation` on the poller. + * Note that the onProgress callback will not be invoked if the operation completes in the first + * request, and attempting to cancel a completed copy will result in an error being thrown. + * + * Example usage: + * ```ts + * const path = "./contoso-allinone.jpg"; + * const readStream = fs.createReadStream(path); + + * const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + * const poller = await client.beginRecognizeReceipts(readStream, "image/jpeg", { + onProgress: (state) => { console.log(`status: ${state.status}`); } + * }); + + * await poller.pollUntilDone(); + * const response = poller.getResult(); + + * console.log("### First receipt:") + * console.log(response.extractedReceipts[0]); + * console.log("### Items:") + * console.log(` \t Quantity\tName\tPrice\tTotalPrice`); + * let i = 1; + * for (const item of response.extractedReceipts[0]?.items) { + * console.log(`${i++})\t ${item.quantity || ""}\t${item.name}\t$${item.totalPrice}`); + * } + * console.log("### Raw 'MerchantAddress' fields:"); + * console.log(response.extractedReceipts[0]?.fields["MerchantAddress"]) + * ``` + * @summary Recognizes receipt information from a given document + * @param {FormRecognizerRequestBody} source Input document + * @param {contentType} Content type of the input. Supported types are "application/pdf", "image/jpeg", "image/png", and "image/tiff"; + * @param {BeginRecognizeReceiptsOptions} [options] Options to the Begin Recognize Receipts operation + */ + public async beginRecognizeReceipts( + source: FormRecognizerRequestBody, + contentType?: ContentType, + options: BeginRecognizeReceiptsOptions = {} + ): Promise { + const analyzePollerClient: RecognizePollerClient = { + beginRecognize: (...args) => recognizeReceiptInternal(this.client, ...args), + getRecognizeResult: (...args) => this.getRecognizedReceipts(...args) + }; + + const poller = new BeginRecognizePoller({ + client: analyzePollerClient, + source: source, + contentType, + ...options + }); + + await poller.poll(); + return poller; + } + + /** + * Recognizes receipt information from a url using pre-built receipt model, enabling you to extract structure data + * from receipts such as merchant name, merchant phone number, transaction date, and more. + * + * This method returns a long running operation poller that allows you to wait + * indefinitely until the copy is completed. + * You can also cancel a copy before it is completed by calling `cancelOperation` on the poller. + * Note that the onProgress callback will not be invoked if the operation completes in the first + * request, and attempting to cancel a completed copy will result in an error being thrown. + * + * Example usage: + * ```ts + * const client = new FormRecognizerClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + * const poller = await client.beginRecognizeReceiptsFromUrl( + * imageUrl, { + * includeTextDetails: true, + * onProgress: (state) => { console.log(`analyzing status: ${state.status}`); } + * }); + * await poller.pollUntilDone(); + * const response = poller.getResult(); + + * console.log("### First receipt:") + * console.log(response.extractedReceipts[0]); + * console.log("### Items:") + * console.log(` \t Quantity\tName\tPrice\tTotalPrice`); + * let i = 1; + * for (const item of response.extractedReceipts[0]?.items) { + * console.log(`${i++})\t ${item.quantity || ""}\t${item.name}\t$${item.totalPrice}`); + * } + * console.log("### Raw 'MerchantAddress' fields:"); + * console.log(response.extractedReceipts[0]?.fields["MerchantAddress"]) + * ``` + * @summary Recognizes receipt information from a given accessible url to input document + * @param {string} documentUrl url to the input document + * @param {BeginRecognizeReceiptsOptions} [options] Options to the Begin Recognize Receipts operation + */ + public async beginRecognizeReceiptsFromUrl( + documentUrl: string, + options: BeginRecognizeReceiptsOptions = {} + ): Promise { + return this.beginRecognizeReceipts(documentUrl, undefined, options); + } + + /** + * @internal + */ + private async getRecognizedReceipts( + resultId: string, + options?: GetRecognizedReceiptsOptions + ): Promise { + const realOptions = options || {}; + const { span, updatedOptions: finalOptions } = createSpan( + "FormRecognizerClient-getRecognizedReceipt", + realOptions + ); + + try { + const result = await this.client.getAnalyzeReceiptResult( + resultId, + operationOptionsToRequestOptionsBase(finalOptions) + ); + return toReceiptResultResponse(result); + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } +} + +/** + * @internal + */ +async function recognizeLayoutInternal( + client: GeneratedClient, + body: FormRecognizerRequestBody, + contentType?: ContentType, + options?: RecognizeContentOptions, + _modelId?: string +): Promise { + const realOptions = options || {}; + const { span, updatedOptions: finalOptions } = createSpan("analyzeLayoutInternal", realOptions); + const requestBody = await toRequestBody(body); + const requestContentType = + contentType !== undefined ? contentType : await getContentType(requestBody); + + try { + return await client.analyzeLayoutAsync({ + contentType: requestContentType, + fileStream: requestBody, + ...operationOptionsToRequestOptionsBase(finalOptions) + }); + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } +} + +/** + * @internal + */ +async function recognizeCustomFormInternal( + client: GeneratedClient, + body: FormRecognizerRequestBody, + contentType?: ContentType, + options: RecognizeFormsOptions = {}, + modelId?: string +): Promise { + const { span, updatedOptions: finalOptions } = createSpan("analyzeCustomFormInternal", options); + const requestBody = await toRequestBody(body); + const requestContentType = + contentType !== undefined ? contentType : await getContentType(requestBody); + + try { + return await client.analyzeWithCustomModel(modelId!, { + contentType: requestContentType, + fileStream: requestBody, + ...operationOptionsToRequestOptionsBase(finalOptions) + }); + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } +} + +/** + * @internal + */ +async function recognizeReceiptInternal( + client: GeneratedClient, + body: FormRecognizerRequestBody, + contentType?: ContentType, + options?: RecognizeReceiptsOptions, + _modelId?: string +): Promise { + const realOptions = options || { includeTextDetails: false }; + const { span, updatedOptions: finalOptions } = createSpan("analyzeReceiptInternal", realOptions); + const requestBody = await toRequestBody(body); + const requestContentType = + contentType !== undefined ? contentType : await getContentType(requestBody); + + try { + return await client.analyzeReceiptAsync({ + contentType: requestContentType, + fileStream: requestBody, + ...operationOptionsToRequestOptionsBase(finalOptions) + }); + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } +} diff --git a/sdk/formrecognizer/ai-form-recognizer/src/formTrainingClient.ts b/sdk/formrecognizer/ai-form-recognizer/src/formTrainingClient.ts new file mode 100644 index 000000000000..184e2d2c2c99 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/formTrainingClient.ts @@ -0,0 +1,562 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/// + +import { + createPipelineFromOptions, + signingPolicy, + InternalPipelineOptions, + operationOptionsToRequestOptionsBase, + RestResponse +} from "@azure/core-http"; +import { PagedAsyncIterableIterator, PageSettings } from "@azure/core-paging"; +import { SDK_VERSION } from "./constants"; +import { logger } from "./logger"; +import { createSpan } from "./tracing"; +import { CanonicalCode } from "@opentelemetry/types"; +import { FormRecognizerClient as GeneratedClient } from "./generated/formRecognizerClient"; +import { + FormRecognizerClientGetCustomModelsResponse as ListModelsResponseModel, + Model, + ModelInfo +} from "./generated/models"; +import { FormRecognizerApiKeyCredential } from "./formRecognizerApiKeyCredential"; +import { TrainPollerClient, BeginTrainingPoller, BeginTrainingPollState } from "./lro/train/poller"; +import { PollOperationState, PollerLike } from "@azure/core-lro"; +import { FormRecognizerClientOptions, FormRecognizerOperationOptions } from './common'; +import { + LabeledFormModelResponse, + FormModelResponse +} from "./models"; + +export { ListModelsResponseModel, Model, ModelInfo, RestResponse }; +/** + * Options for the list models operation. + */ +export type ListModelsOptions = FormRecognizerOperationOptions; + +/** + * Options for the get summary operation. + */ +export type GetSummaryOptions = FormRecognizerOperationOptions; + +/** + * Options for the delete model operation. + */ +export type DeleteModelOptions = FormRecognizerOperationOptions; + +/** + * Options for the get model operation. + */ +export type GetModelOptions = FormRecognizerOperationOptions; + +/** + * Options for the get model operation. + */ +export type GetLabeledModelOptions = FormRecognizerOperationOptions & { + includeKeys?: boolean; +}; + +/** + * Options for traing models + */ +export type TrainModelOptions = FormRecognizerOperationOptions & { + prefix?: string; + includeSubFolders?: boolean; +}; + +/** + * Options for the begin training model operation. + */ +export type BeginTrainingOptions = TrainModelOptions & { + intervalInMs?: number; + onProgress?: (state: BeginTrainingPollState) => void; + resumeFrom?: string; +}; + +/** + * Options for the begin training with labels operation. + */ +export type BeginTrainingWithLabelsOptions = FormRecognizerOperationOptions & { + prefix?: string; + includeSubFolders?: boolean; +}; + + +/** + * Client class for Form training operations and Form model management. + */ +export class FormTrainingClient { + /** + * The URL to Azure Form Recognizer service endpoint + */ + public readonly endpointUrl: string; + + /** + * @internal + * @ignore + * A reference to the auto-generated FormRecognizer HTTP client. + */ + private readonly client: GeneratedClient; + + /** + * Creates an instance of FormTrainingClient. + * + * Example usage: + * ```ts + * import {FormTrainingClient, FormRecognizerApiKeyCredential } from "@azure/ai-form-recognizer"; + * + * const client = new FormTrainingClient( + * "", + * new FormRecognizerApiKeyCredential("") + * ); + * ``` + * @param {string} endpointUrl The URL to Azure Form Recognizer service endpoint + * @param {FormRecognizerApiKeyCredential} credential Used to authenticate requests to the service. + * @param {FormRecognizerClientOptions} [options] Used to configure the client. + */ + constructor( + endpointUrl: string, + credential: FormRecognizerApiKeyCredential, + options: FormRecognizerClientOptions = {} + ) { + this.endpointUrl = endpointUrl; + const { ...pipelineOptions } = options; + + const libInfo = `azsdk-js-ai-formrecognizer/${SDK_VERSION}`; + if (!pipelineOptions.userAgentOptions) { + pipelineOptions.userAgentOptions = {}; + } + if (pipelineOptions.userAgentOptions.userAgentPrefix) { + pipelineOptions.userAgentOptions.userAgentPrefix = `${pipelineOptions.userAgentOptions.userAgentPrefix} ${libInfo}`; + } else { + pipelineOptions.userAgentOptions.userAgentPrefix = libInfo; + } + + const authPolicy = signingPolicy(credential); + + const internalPipelineOptions: InternalPipelineOptions = { + ...pipelineOptions, + ...{ + loggingOptions: { + logger: logger.info, + allowedHeaderNames: ["x-ms-correlation-request-id", "x-ms-request-id"] + } + } + }; + + const pipeline = createPipelineFromOptions(internalPipelineOptions, authPolicy); + this.client = new GeneratedClient(credential, this.endpointUrl, pipeline); + } + /** + * Retrieves summary information about the cognitive service account + * + * @param {GetSummaryOptions} options Options to GetSummary operation + */ + public async getSummary(options?: GetSummaryOptions) { + const realOptions: ListModelsOptions = options || {}; + const { span, updatedOptions: finalOptions } = createSpan( + "FormTrainingClient-listCustomModels", + realOptions + ); + + try { + const result = await this.client.getCustomModels({ + ...operationOptionsToRequestOptionsBase(finalOptions) + }); + + return result; + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } + + /** + * Mark model for deletion. Model artifacts will be permanently removed within 48 hours. + * + * @param {string} modelId Id of the model to mark for deletion + * @param {DeleteModelOptions} options Options to the Delete Model operation + */ + public async deleteModel(modelId: string, options?: DeleteModelOptions): Promise { + const realOptions = options || {}; + const { span, updatedOptions: finalOptions } = createSpan( + "FormTrainingClient-deleteModel", + realOptions + ); + + try { + return await this.client.deleteCustomModel( + modelId, + operationOptionsToRequestOptionsBase(finalOptions) + ); + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } + + /** + * Get detailed information about a model from unsupervised training. + * + * @param {string} modelId Id of the model to get information + * @param {GetModelOptions} options Options to the Get Model operation + */ + public async getModel( + modelId: string, + options: GetModelOptions = {} + ): Promise { + const realOptions = options || {}; + const { span, updatedOptions: finalOptions } = createSpan( + "FormTrainingClient-getModel", + realOptions + ); + + try { + const respnose = await this.client.getCustomModel(modelId, { + ...operationOptionsToRequestOptionsBase(finalOptions), + // Include keys is always set to true -- the service does not have a use case for includeKeys: false. + includeKeys: true + }); + if ( + respnose.modelInfo.status === "ready" && + (respnose.trainResult?.averageModelAccuracy || respnose.trainResult?.fields) + ) { + throw new Error(`The model ${modelId} is trained with labels.`); + } else { + return (respnose as unknown) as FormModelResponse; + } + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } + + /** + * Get detailed information about a model from supervised training using labels. + * + * @param {string} modelId Id of the model to get information + * @param {GetModelOptions} options Options to the Get Labeled Model operation + */ + public async getLabeledModel( + modelId: string, + options: GetLabeledModelOptions = {} + ): Promise { + const realOptions = options || {}; + const { span, updatedOptions: finalOptions } = createSpan( + "FormTrainingClient-getLabeledModel", + realOptions + ); + + try { + const response = await this.client.getCustomModel( + modelId, + operationOptionsToRequestOptionsBase(finalOptions) + ); + + if (response.modelInfo.status === "ready") { + if (response.keys) { + throw new Error(`The model ${modelId} is not rained with labels.`); + } + } + + return (response as unknown) as LabeledFormModelResponse; + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } + + private async *listModelsPage( + _settings: PageSettings, + options: ListModelsOptions = {} + ): AsyncIterableIterator { + let result = await this.list(options); + yield result; + + while (result.nextLink) { + result = await this.listNextPage(result.nextLink, options); + yield result; + } + } + + private async *listModelsAll( + settings: PageSettings, + options: ListModelsOptions = {} + ): AsyncIterableIterator { + for await (const page of this.listModelsPage(settings, options)) { + yield* page.modelList || []; + } + } + + /** + * Returns an async iterable iterator to list information about all models in the cognitive service account. + * + * .byPage() returns an async iterable iterator to list the blobs in pages. + * + * Example using `for await` syntax: + * + * ```js + * const client = new FormTrainingClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + * const result = client.listModels(); + * let i = 1; + * for await (const model of result) { + * console.log(`model ${i++}:`); + * console.log(model); + * } + * ``` + * + * Example using `iter.next()`: + * + * ```js + * let i = 1; + * let iter = client.listModels(); + * let modelItem = await iter.next(); + * while (!modelItem.done) { + * console.log(`model ${i++}: ${modelItem.value}`); + * modelItem = await iter.next(); + * } + * ``` + * + * Example using `byPage()`: + * + * ```js + * let i = 1; + * for await (const response of client.listModels().byPage()) { + * for (const modelInfo of response.modelList!) { + * console.log(`model ${i++}: ${modelInfo.modelId}`); + * } + * } + * ``` + * + * @param {ListModelOptions} options Options to the List Models operation + */ + public listModels( + options: ListModelsOptions = {} + ): PagedAsyncIterableIterator { + const iter = this.listModelsAll({}, options); + + return { + next() { + return iter.next(); + }, + + [Symbol.asyncIterator]() { + return this; + }, + + byPage: (settings: PageSettings = {}) => { + return this.listModelsPage(settings, options); + } + }; + } + + private async list(options?: ListModelsOptions): Promise { + const realOptions: ListModelsOptions = options || {}; + const { span, updatedOptions: finalOptions } = createSpan( + "FormTrainingClient-list", + realOptions + ); + + try { + const result = await this.client.listCustomModels({ + ...operationOptionsToRequestOptionsBase(finalOptions) + }); + + return result; + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } + + private async listNextPage( + nextLink: string, + options?: ListModelsOptions + ): Promise { + const realOptions: ListModelsOptions = options || {}; + const { span, updatedOptions: finalOptions } = createSpan( + "FormTrainingClient-listNextPage", + realOptions + ); + + try { + const result = await this.client.listCustomModelsNext(nextLink, { + ...operationOptionsToRequestOptionsBase(finalOptions) + }); + + return result; + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } + + /** + * Creates and trains a model without using labels (i.e., unsupervised). + * This method returns a long running operation poller that allows you to wait + * indefinitely until the copy is completed. + * You can also cancel a copy before it is completed by calling `cancelOperation` on the poller. + * Note that the onProgress callback will not be invoked if the operation completes in the first + * request, and attempting to cancel a completed copy will result in an error being thrown. + * + * Example usage: + * ```ts + * const dataSourceUri = process.env["DOCUMENT_SOURCE"] || ""; + * const client = new FormTrainingClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + * + * const poller = await client.beginTraining(dataSourceUri, { + * onProgress: (state) => { console.log(`training status: ${state.status}`); } + * }); + * await poller.pollUntilDone(); + * const response = poller.getResult(); + * console.log(response.modelInfo.modelId); + * ``` + * @summary Creats and trains a model + * @param {string} source Accessible Uri to an Azure Storage Blob container storing the training documents + * @param {BeginTrainingOptions} [options] Options to the BeginTraining operation + */ + public async beginTraining( + source: string, + options: BeginTrainingOptions = {} + ): Promise, FormModelResponse>> { + const trainPollerClient: TrainPollerClient = { + getModel: (modelId: string, options: GetModelOptions) => this.getModel(modelId, options), + trainCustomModelInternal: ( + source: string, + _useLabelFile?: boolean, + options?: TrainModelOptions + ) => trainCustomModelInternal(this.client, source, false, options) + }; + + const poller = new BeginTrainingPoller({ + client: trainPollerClient, + source, + intervalInMs: options.intervalInMs, + onProgress: options.onProgress, + resumeFrom: options.resumeFrom, + trainModelOptions: options + }); + + await poller.poll(); + return poller; + } + + /** + * Creates and trains a model using labels (i.e., supervised). + * This method returns a long running operation poller that allows you to wait + * indefinitely until the copy is completed. + * You can also cancel a copy before it is completed by calling `cancelOperation` on the poller. + * Note that the onProgress callback will not be invoked if the operation completes in the first + * request, and attempting to cancel a completed copy will result in an error being thrown. + * + * Example usage: + * ```ts + * const dataSourceUri = process.env["DOCUMENT_SOURCE"] || ""; + * const client = new FormTrainingClient(endpoint, new FormRecognizerApiKeyCredential(apiKey)); + * + * const poller = await client.beginTrainingWithLabel(dataSourceUri, { + * onProgress: (state) => { console.log(`training status: ${state.status}`); } + * }); + * await poller.pollUntilDone(); + * const response = poller.getResult(); + * console.log(response.modelInfo.modelId); + * ``` + * @summary Creats and trains a model + * @param {string} source Accessible Uri to an Azure Storage Blob container storing the training documents and label files + * @param {BeginTrainingOptions} [options] Options to the BeginTraining operation + */ + public async beginTrainingWithLabel( + source: string, + options: BeginTrainingOptions = {} + ): Promise, LabeledFormModelResponse>> { + const trainPollerClient: TrainPollerClient = { + getModel: (modelId: string, options: GetModelOptions) => + this.getLabeledModel(modelId, options), + trainCustomModelInternal: ( + source: string, + _useLabelFile?: boolean, + options?: TrainModelOptions + ) => trainCustomModelInternal(this.client, source, true, options) + }; + + const poller = new BeginTrainingPoller({ + client: trainPollerClient, + source, + intervalInMs: options.intervalInMs, + onProgress: options.onProgress, + resumeFrom: options.resumeFrom, + trainModelOptions: options + }); + + await poller.poll(); + return poller; + } +} + +async function trainCustomModelInternal( + client: GeneratedClient, + source: string, + useLabelFile?: boolean, + options?: TrainModelOptions +) { + const realOptions = options || {}; + const { span, updatedOptions: finalOptions } = createSpan( + "trainCustomModelInternal", + realOptions + ); + + try { + const requestBody = { + source: source, + sourceFilter: { + prefix: realOptions.prefix, + includeSubFolders: realOptions.includeSubFolders + }, + useLabelFile + }; + return await client.trainCustomModelAsync( + requestBody, + operationOptionsToRequestOptionsBase(finalOptions) + ); + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } +} diff --git a/sdk/formrecognizer/ai-form-recognizer/src/generated/formRecognizerClient.ts b/sdk/formrecognizer/ai-form-recognizer/src/generated/formRecognizerClient.ts new file mode 100644 index 000000000000..cbe4e5758317 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/generated/formRecognizerClient.ts @@ -0,0 +1,493 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is regenerated. + */ + +import * as coreHttp from "@azure/core-http"; +import * as Parameters from "./models/parameters"; +import * as Models from "./models"; +import * as Mappers from "./models/mappers"; +import { FormRecognizerClientContext } from "./formRecognizerClientContext"; + +class FormRecognizerClient extends FormRecognizerClientContext { + /** + * Initializes a new instance of the FormRecognizerClient class. + * @param credentials Subscription credentials which uniquely identify client subscription. + * @param endpoint Supported Cognitive Services endpoints (protocol and hostname, for example: + * https://westus2.api.cognitive.microsoft.com). + * @param options The parameter options + */ + constructor( + credentials: coreHttp.TokenCredential | coreHttp.ServiceClientCredentials, + endpoint: string, + options?: Models.FormRecognizerClientOptionalParams + ) { + super(credentials, endpoint, options); + } + + /** + * Create and train a custom model. The request must include a source parameter that is either an + * externally accessible Azure storage blob container Uri (preferably a Shared Access Signature Uri) or + * valid path to a data folder in a locally mounted drive. When local paths are specified, they must + * follow the Linux/Unix path format and be an absolute path rooted to the input mount configuration + * setting value e.g., if '{Mounts:Input}' configuration setting value is '/input' then a valid source + * path would be '/input/contosodataset'. All data to be trained is expected to be under the source + * folder or sub folders under it. Models are trained using documents that are of the following content + * type - 'application/pdf', 'image/jpeg', 'image/png', 'image/tiff'. Other type of content is ignored. + * @param trainRequest Training request parameters. + * @param options The options parameters. + */ + trainCustomModelAsync( + trainRequest: Models.TrainRequest, + options?: coreHttp.OperationOptions + ): Promise { + return this.sendOperationRequest( + { trainRequest, options }, + trainCustomModelAsyncOperationSpec + ) as Promise; + } + + /** + * Get detailed information about a custom model. + * @param modelId Model identifier. + * @param options The options parameters. + */ + getCustomModel( + modelId: string, + options?: Models.FormRecognizerClientGetCustomModelOptionalParams + ): Promise { + return this.sendOperationRequest( + { modelId, options }, + getCustomModelOperationSpec + ) as Promise; + } + + /** + * Mark model for deletion. Model artifacts will be permanently removed within a predetermined period. + * @param modelId Model identifier. + * @param options The options parameters. + */ + deleteCustomModel( + modelId: string, + options?: coreHttp.OperationOptions + ): Promise { + return this.sendOperationRequest( + { modelId, options }, + deleteCustomModelOperationSpec + ) as Promise; + } + + /** + * Extract key-value pairs, tables, and semantic values from a given document. The input document must + * be of one of the supported content types - 'application/pdf', 'image/jpeg', 'image/png' or + * 'image/tiff'. Alternatively, use 'application/json' type to specify the location (Uri or local path) + * of the document to be analyzed. + * @param modelId Model identifier. + * @param options The options parameters. + */ + analyzeWithCustomModel( + modelId: string, + options?: Models.FormRecognizerClientAnalyzeWithCustomModelOptionalParams + ): Promise { + let operationSpec: coreHttp.OperationSpec; + if ( + options && + "contentType" in options && + ["application/pdf", "image/jpeg", "image/png", "image/tiff"].indexOf( + options.contentType ?? "" + ) > -1 + ) { + operationSpec = analyzeWithCustomModel$binaryOperationSpec; + } else { + operationSpec = analyzeWithCustomModel$jsonOperationSpec; + } + return this.sendOperationRequest( + { modelId, options }, + operationSpec + ) as Promise; + } + + /** + * Obtain current status and the result of the analyze form operation. + * @param modelId Model identifier. + * @param resultId Analyze operation result identifier. + * @param options The options parameters. + */ + getAnalyzeFormResult( + modelId: string, + resultId: string, + options?: coreHttp.OperationOptions + ): Promise { + return this.sendOperationRequest( + { modelId, resultId, options }, + getAnalyzeFormResultOperationSpec + ) as Promise; + } + + /** + * Extract field text and semantic values from a given receipt document. The input document must be of + * one of the supported content types - 'application/pdf', 'image/jpeg', 'image/png' or 'image/tiff'. + * Alternatively, use 'application/json' type to specify the location (Uri or local path) of the + * document to be analyzed. + * @param options The options parameters. + */ + analyzeReceiptAsync( + options?: Models.FormRecognizerClientAnalyzeReceiptAsyncOptionalParams + ): Promise { + let operationSpec: coreHttp.OperationSpec; + if ( + options && + "contentType" in options && + ["application/pdf", "image/jpeg", "image/png", "image/tiff"].indexOf( + options.contentType ?? "" + ) > -1 + ) { + operationSpec = analyzeReceiptAsync$binaryOperationSpec; + } else { + operationSpec = analyzeReceiptAsync$jsonOperationSpec; + } + return this.sendOperationRequest({ options }, operationSpec) as Promise< + Models.FormRecognizerClientAnalyzeReceiptAsyncResponse + >; + } + + /** + * Track the progress and obtain the result of the analyze receipt operation. + * @param resultId Analyze operation result identifier. + * @param options The options parameters. + */ + getAnalyzeReceiptResult( + resultId: string, + options?: coreHttp.OperationOptions + ): Promise { + return this.sendOperationRequest( + { resultId, options }, + getAnalyzeReceiptResultOperationSpec + ) as Promise; + } + + /** + * Extract text and layout information from a given document. The input document must be of one of the + * supported content types - 'application/pdf', 'image/jpeg', 'image/png' or 'image/tiff'. + * Alternatively, use 'application/json' type to specify the location (Uri or local path) of the + * document to be analyzed. + * @param options The options parameters. + */ + analyzeLayoutAsync( + options?: Models.FormRecognizerClientAnalyzeLayoutAsyncOptionalParams + ): Promise { + let operationSpec: coreHttp.OperationSpec; + if ( + options && + "contentType" in options && + ["application/pdf", "image/jpeg", "image/png", "image/tiff"].indexOf( + options.contentType ?? "" + ) > -1 + ) { + operationSpec = analyzeLayoutAsync$binaryOperationSpec; + } else { + operationSpec = analyzeLayoutAsync$jsonOperationSpec; + } + return this.sendOperationRequest({ options }, operationSpec) as Promise< + Models.FormRecognizerClientAnalyzeLayoutAsyncResponse + >; + } + + /** + * Track the progress and obtain the result of the analyze layout operation + * @param resultId Analyze operation result identifier. + * @param options The options parameters. + */ + getAnalyzeLayoutResult( + resultId: string, + options?: coreHttp.OperationOptions + ): Promise { + return this.sendOperationRequest( + { resultId, options }, + getAnalyzeLayoutResultOperationSpec + ) as Promise; + } + + /** + * Get information about all custom models + * @param options The options parameters. + */ + listCustomModels( + options?: coreHttp.OperationOptions + ): Promise { + return this.sendOperationRequest( + { options }, + listCustomModelsOperationSpec + ) as Promise; + } + + /** + * Get information about all custom models + * @param options The options parameters. + */ + getCustomModels( + options?: coreHttp.OperationOptions + ): Promise { + return this.sendOperationRequest( + { options }, + getCustomModelsOperationSpec + ) as Promise; + } + + /** + * ListCustomModelsNext + * @param nextLink The nextLink from the previous successful call to the ListCustomModels method. + * @param options The options parameters. + */ + listCustomModelsNext( + nextLink: string, + options?: coreHttp.OperationOptions + ): Promise { + return this.sendOperationRequest( + { nextLink, options }, + listCustomModelsNextOperationSpec + ) as Promise; + } +} +// Operation Specifications + +const serializer = new coreHttp.Serializer(Mappers, /* isXml */ false); + +const trainCustomModelAsyncOperationSpec: coreHttp.OperationSpec = { + path: "/custom/models", + httpMethod: "POST", + responses: { + 201: { + headersMapper: Mappers.FormRecognizerClientTrainCustomModelAsyncHeaders + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + requestBody: Parameters.trainRequest, + urlParameters: [Parameters.endpoint], + serializer +}; +const getCustomModelOperationSpec: coreHttp.OperationSpec = { + path: "/custom/models/{modelId}", + httpMethod: "GET", + responses: { + 200: { + bodyMapper: Mappers.Model + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + queryParameters: [Parameters.includeKeys], + urlParameters: [Parameters.endpoint, Parameters.modelId], + serializer +}; +const deleteCustomModelOperationSpec: coreHttp.OperationSpec = { + path: "/custom/models/{modelId}", + httpMethod: "DELETE", + responses: { + 204: {}, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + urlParameters: [Parameters.endpoint, Parameters.modelId], + serializer +}; +const analyzeWithCustomModel$binaryOperationSpec: coreHttp.OperationSpec = { + path: "/custom/models/{modelId}/analyze", + httpMethod: "POST", + responses: { + 202: { + headersMapper: Mappers.FormRecognizerClientAnalyzeWithCustomModelHeaders + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + requestBody: Parameters.fileStream, + queryParameters: [Parameters.includeTextDetails], + urlParameters: [Parameters.endpoint, Parameters.modelId], + headerParameters: [Parameters.contentType], + serializer +}; +const analyzeWithCustomModel$jsonOperationSpec: coreHttp.OperationSpec = { + path: "/custom/models/{modelId}/analyze", + httpMethod: "POST", + responses: { + 202: { + headersMapper: Mappers.FormRecognizerClientAnalyzeWithCustomModelHeaders + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + requestBody: Parameters.fileStream1, + queryParameters: [Parameters.includeTextDetails], + urlParameters: [Parameters.endpoint, Parameters.modelId], + serializer +}; +const getAnalyzeFormResultOperationSpec: coreHttp.OperationSpec = { + path: "/custom/models/{modelId}/analyzeResults/{resultId}", + httpMethod: "GET", + responses: { + 200: { + bodyMapper: Mappers.AnalyzeOperationResult + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + urlParameters: [Parameters.endpoint, Parameters.modelId, Parameters.resultId], + serializer +}; +const analyzeReceiptAsync$binaryOperationSpec: coreHttp.OperationSpec = { + path: "/prebuilt/receipt/analyze", + httpMethod: "POST", + responses: { + 202: { + headersMapper: Mappers.FormRecognizerClientAnalyzeReceiptAsyncHeaders + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + requestBody: Parameters.fileStream, + queryParameters: [Parameters.includeTextDetails], + urlParameters: [Parameters.endpoint], + headerParameters: [Parameters.contentType], + serializer +}; +const analyzeReceiptAsync$jsonOperationSpec: coreHttp.OperationSpec = { + path: "/prebuilt/receipt/analyze", + httpMethod: "POST", + responses: { + 202: { + headersMapper: Mappers.FormRecognizerClientAnalyzeReceiptAsyncHeaders + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + requestBody: Parameters.fileStream1, + queryParameters: [Parameters.includeTextDetails], + urlParameters: [Parameters.endpoint], + serializer +}; +const getAnalyzeReceiptResultOperationSpec: coreHttp.OperationSpec = { + path: "/prebuilt/receipt/analyzeResults/{resultId}", + httpMethod: "GET", + responses: { + 200: { + bodyMapper: Mappers.AnalyzeOperationResult + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + urlParameters: [Parameters.endpoint, Parameters.resultId], + serializer +}; +const analyzeLayoutAsync$binaryOperationSpec: coreHttp.OperationSpec = { + path: "/layout/analyze", + httpMethod: "POST", + responses: { + 202: { + headersMapper: Mappers.FormRecognizerClientAnalyzeLayoutAsyncHeaders + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + requestBody: Parameters.fileStream, + urlParameters: [Parameters.endpoint], + headerParameters: [Parameters.contentType], + serializer +}; +const analyzeLayoutAsync$jsonOperationSpec: coreHttp.OperationSpec = { + path: "/layout/analyze", + httpMethod: "POST", + responses: { + 202: { + headersMapper: Mappers.FormRecognizerClientAnalyzeLayoutAsyncHeaders + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + requestBody: Parameters.fileStream1, + urlParameters: [Parameters.endpoint], + serializer +}; +const getAnalyzeLayoutResultOperationSpec: coreHttp.OperationSpec = { + path: "/layout/analyzeResults/{resultId}", + httpMethod: "GET", + responses: { + 200: { + bodyMapper: Mappers.AnalyzeOperationResult + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + urlParameters: [Parameters.endpoint, Parameters.resultId], + serializer +}; +const listCustomModelsOperationSpec: coreHttp.OperationSpec = { + path: "/custom/models", + httpMethod: "GET", + responses: { + 200: { + bodyMapper: Mappers.Models + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + queryParameters: [Parameters.op], + urlParameters: [Parameters.endpoint], + serializer +}; +const getCustomModelsOperationSpec: coreHttp.OperationSpec = { + path: "/custom/models", + httpMethod: "GET", + responses: { + 200: { + bodyMapper: Mappers.Models + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + queryParameters: [Parameters.op1], + urlParameters: [Parameters.endpoint], + serializer +}; +const listCustomModelsNextOperationSpec: coreHttp.OperationSpec = { + path: "{nextLink}", + httpMethod: "GET", + responses: { + 200: { + bodyMapper: Mappers.Models + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + queryParameters: [Parameters.op], + urlParameters: [Parameters.endpoint, Parameters.nextLink], + serializer +}; + +// Operation Specifications + +export { + FormRecognizerClient, + FormRecognizerClientContext, + Models as FormRecognizerModels, + Mappers as FormRecognizerMappers +}; diff --git a/sdk/formrecognizer/ai-form-recognizer/src/generated/formRecognizerClientContext.ts b/sdk/formrecognizer/ai-form-recognizer/src/generated/formRecognizerClientContext.ts new file mode 100644 index 000000000000..f25a642e77ec --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/generated/formRecognizerClientContext.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is regenerated. + */ + +import * as coreHttp from "@azure/core-http"; +import * as Models from "./models"; + +const packageName = "@azure/ai-form-recognizer"; +const packageVersion = "1.0.0-preview.1"; + +export class FormRecognizerClientContext extends coreHttp.ServiceClient { + endpoint: string; + + /** + * Initializes a new instance of the FormRecognizerClientContext class. + * @param credentials Subscription credentials which uniquely identify client subscription. + * @param endpoint Supported Cognitive Services endpoints (protocol and hostname, for example: + * https://westus2.api.cognitive.microsoft.com). + * @param options The parameter options + */ + constructor( + credentials: coreHttp.TokenCredential | coreHttp.ServiceClientCredentials, + endpoint: string, + options?: Models.FormRecognizerClientOptionalParams + ) { + if (credentials === undefined) { + throw new Error("'credentials' cannot be null"); + } + if (endpoint === undefined) { + throw new Error("'endpoint' cannot be null"); + } + + // Initializing default values for options + if (!options) { + options = {}; + } + + if (!options.userAgent) { + const defaultUserAgent = coreHttp.getDefaultUserAgentValue(); + options.userAgent = `${packageName}/${packageVersion} ${defaultUserAgent}`; + } + + super(credentials, options); + + this.requestContentType = "application/json; charset=utf-8"; + + this.baseUri = options.endpoint || "{endpoint}/formrecognizer/v2.0-preview"; + + // Parameter assignments + this.endpoint = endpoint; + } +} diff --git a/sdk/formrecognizer/ai-form-recognizer/src/generated/models/index.ts b/sdk/formrecognizer/ai-form-recognizer/src/generated/models/index.ts new file mode 100644 index 000000000000..6c642baf8b61 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/generated/models/index.ts @@ -0,0 +1,922 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is regenerated. + */ + +import * as coreHttp from "@azure/core-http"; + +/** + * Request parameter to train a new custom model. + */ +export interface TrainRequest { + /** + * Source path containing the training documents. + */ + source: string; + /** + * Filter to apply to the documents in the source path for training. + */ + sourceFilter?: TrainSourceFilter; + /** + * Use label file for training a model. + */ + useLabelFile?: boolean; +} + +/** + * Filter to apply to the documents in the source path for training. + */ +export interface TrainSourceFilter { + /** + * A case-sensitive prefix string to filter documents in the source path for training. For example, when using a Azure storage blob Uri, use the prefix to restrict sub folders for training. + */ + prefix?: string; + /** + * A flag to indicate if sub folders within the set of prefix folders will also need to be included when searching for content to be preprocessed. + */ + includeSubFolders?: boolean; +} + +export interface ErrorResponse { + error: ErrorInformation; +} + +export interface ErrorInformation { + code: string; + message: string; +} + +/** + * Response to the get custom model operation. + */ +export interface Model { + /** + * Basic custom model information. + */ + modelInfo: ModelInfo; + /** + * Keys extracted by the custom model. + */ + keys?: KeysResult; + /** + * Custom model training result. + */ + trainResult?: TrainResult; +} + +/** + * Basic custom model information. + */ +export interface ModelInfo { + /** + * Model identifier. + */ + modelId: string; + /** + * Status of the model. + */ + status: ModelStatus; + /** + * Date and time (UTC) when the model was created. + */ + createdOn: Date; + /** + * Date and time (UTC) when the status was last updated. + */ + lastUpdatedOn: Date; +} + +/** + * Keys extracted by the custom model. + */ +export interface KeysResult { + /** + * Object mapping clusterIds to a list of keys. + */ + clusters: { [propertyName: string]: string[] }; +} + +/** + * Custom model training result. + */ +export interface TrainResult { + /** + * List of the documents used to train the model and any errors reported in each document. + */ + trainingDocuments: TrainingDocumentInfo[]; + /** + * List of fields used to train the model and the train operation error reported by each. + */ + fields?: FormFieldsReport[]; + /** + * Average accuracy. + */ + averageModelAccuracy?: number; + /** + * Errors returned during the training operation. + */ + errors?: ErrorInformation[]; +} + +/** + * Report for a custom model training document. + */ +export interface TrainingDocumentInfo { + /** + * Training document name. + */ + documentName: string; + /** + * Total number of pages trained. + */ + pages: number; + /** + * List of errors. + */ + errors: ErrorInformation[]; + /** + * Status of the training operation. + */ + status: TrainStatus; +} + +/** + * Report for a custom model training field. + */ +export interface FormFieldsReport { + /** + * Training field name. + */ + fieldName: string; + /** + * Estimated extraction accuracy for this field. + */ + accuracy: number; +} + +/** + * Uri or local path to source data. + */ +export interface SourcePath { + /** + * File source path. + */ + source?: string; +} + +/** + * Status and result of the queued analyze operation. + */ +export interface AnalyzeOperationResult { + /** + * Operation status. + */ + status: OperationStatus; + /** + * Date and time (UTC) when the analyze operation was submitted. + */ + createdOn: Date; + /** + * Date and time (UTC) when the status was last updated. + */ + lastUpdatedOn: Date; + /** + * Results of the analyze operation. + */ + analyzeResult?: AnalyzeResult; +} + +/** + * Analyze operation result. + */ +export interface AnalyzeResult { + /** + * Version of schema used for this result. + */ + version: string; + /** + * Text extracted from the input. + */ + readResults: ReadResult[]; + /** + * Page-level information extracted from the input. + */ + pageResults?: PageResult[]; + /** + * Document-level information extracted from the input. + */ + documentResults?: DocumentResult[]; + /** + * List of errors reported during the analyze operation. + */ + errors?: ErrorInformation[]; +} + +/** + * Text extracted from a page in the input document. + */ +export interface ReadResult { + /** + * The 1-based page number in the input document. + */ + pageNumber: number; + /** + * The general orientation of the text in clockwise direction, measured in degrees between (-180, 180]. + */ + angle: number; + /** + * The width of the image/PDF in pixels/inches, respectively. + */ + width: number; + /** + * The height of the image/PDF in pixels/inches, respectively. + */ + height: number; + /** + * The unit used by the width, height and boundingBox properties. For images, the unit is "pixel". For PDF, the unit is "inch". + */ + unit: LengthUnit; + /** + * The detected language on the page overall. + */ + language?: Language; + /** + * When includeTextDetails is set to true, a list of recognized text lines. The maximum number of lines returned is 300 per page. The lines are sorted top to bottom, left to right, although in certain cases proximity is treated with higher priority. As the sorting order depends on the detected text, it may change across images and OCR version updates. Thus, business logic should be built upon the actual line location instead of order. + */ + lines?: TextLine[]; +} + +/** + * An object representing an extracted text line. + */ +export interface TextLine { + /** + * The text content of the line. + */ + text: string; + /** + * Bounding box of an extracted line. + */ + boundingBox: number[]; + /** + * The detected language of this line, if different from the overall page language. + */ + language?: Language; + /** + * List of words in the text line. + */ + words: TextWord[]; +} + +/** + * An object representing a word. + */ +export interface TextWord { + /** + * The text content of the word. + */ + text: string; + /** + * Bounding box of an extracted word. + */ + boundingBox: number[]; + /** + * Confidence value. + */ + confidence?: number; +} + +/** + * Extracted information from a single page. + */ +export interface PageResult { + /** + * Page number. + */ + pageNumber: number; + /** + * Cluster identifier. + */ + clusterId?: number; + /** + * List of key-value pairs extracted from the page. + */ + keyValuePairs?: KeyValuePair[]; + /** + * List of data tables extracted from the page. + */ + tables?: DataTable[]; +} + +/** + * Information about the extracted key-value pair. + */ +export interface KeyValuePair { + /** + * A user defined label for the key/value pair entry. + */ + label?: string; + /** + * Information about the extracted key in a key-value pair. + */ + key: KeyValueElement; + /** + * Information about the extracted value in a key-value pair. + */ + value: KeyValueElement; + /** + * Confidence value. + */ + confidence: number; +} + +/** + * Information about the extracted key or value in a key-value pair. + */ +export interface KeyValueElement { + /** + * The text content of the key or value. + */ + text: string; + /** + * Bounding box of the key or value. + */ + boundingBox?: number[]; + /** + * When includeTextDetails is set to true, a list of references to the text elements constituting this key or value. + */ + elements?: string[]; +} + +/** + * Information about the extracted table contained in a page. + */ +export interface DataTable { + /** + * Number of rows. + */ + rows: number; + /** + * Number of columns. + */ + columns: number; + /** + * List of cells contained in the table. + */ + cells: DataTableCell[]; +} + +/** + * Information about the extracted cell in a table. + */ +export interface DataTableCell { + /** + * Row index of the cell. + */ + rowIndex: number; + /** + * Column index of the cell. + */ + columnIndex: number; + /** + * Number of rows spanned by this cell. + */ + rowSpan?: number; + /** + * Number of columns spanned by this cell. + */ + columnSpan?: number; + /** + * Text content of the cell. + */ + text: string; + /** + * Bounding box of the cell. + */ + boundingBox: number[]; + /** + * Confidence value. + */ + confidence: number; + /** + * When includeTextDetails is set to true, a list of references to the text elements constituting this table cell. + */ + elements?: string[]; + /** + * Is the current cell a header cell? + */ + isHeader?: boolean; + /** + * Is the current cell a footer cell? + */ + isFooter?: boolean; +} + +/** + * A set of extracted fields corresponding to the input document. + */ +export interface DocumentResult { + /** + * Document type. + */ + docType: string; + /** + * First and last page number where the document is found. + */ + pageRange: number[]; + /** + * Dictionary of named field values. + */ + fields: { [propertyName: string]: FieldValue }; +} + +/** + * Recognized field value. + */ +export interface FieldValue { + /** + * Type of field value. + */ + type: FieldValueType; + /** + * String value. + */ + valueString?: string; + /** + * Date value. + */ + valueDate?: Date; + /** + * Time value. + */ + valueTime?: string; + /** + * Phone number value. + */ + valuePhoneNumber?: string; + /** + * Floating point value. + */ + valueNumber?: number; + /** + * Integer value. + */ + valueInteger?: number; + /** + * Array of field values. + */ + valueArray?: FieldValue[]; + /** + * Dictionary of named field values. + */ + valueObject?: { [propertyName: string]: FieldValue }; + /** + * Text content of the extracted field. + */ + text?: string; + /** + * Bounding box of the field value, if appropriate. + */ + boundingBox?: number[]; + /** + * Confidence score. + */ + confidence?: number; + /** + * When includeTextDetails is set to true, a list of references to the text elements constituting this field. + */ + elements?: string[]; + /** + * The 1-based page number in the input document. + */ + pageNumber?: number; +} + +/** + * Response to the list custom models operation. + */ +export interface Models { + /** + * Summary of all trained custom models. + */ + summary?: ModelsSummary; + /** + * Collection of trained custom models. + */ + modelList?: ModelInfo[]; + /** + * Link to the next page of custom models. + */ + nextLink?: string; +} + +/** + * Summary of all trained custom models. + */ +export interface ModelsSummary { + /** + * Current count of trained custom models. + */ + count: number; + /** + * Max number of models that can be trained for this account. + */ + limit: number; + /** + * Date and time (UTC) when the summary was last updated. + */ + lastUpdatedOn: Date; +} + +/** + * Defines headers for formRecognizerClient_trainCustomModelAsync operation. + */ +export interface FormRecognizerClientTrainCustomModelAsyncHeaders { + location?: string; +} + +/** + * Defines headers for formRecognizerClient_analyzeWithCustomModel operation. + */ +export interface FormRecognizerClientAnalyzeWithCustomModelHeaders { + operationLocation?: string; +} + +/** + * Defines headers for formRecognizerClient_analyzeReceiptAsync operation. + */ +export interface FormRecognizerClientAnalyzeReceiptAsyncHeaders { + operationLocation?: string; +} + +/** + * Defines headers for formRecognizerClient_analyzeLayoutAsync operation. + */ +export interface FormRecognizerClientAnalyzeLayoutAsyncHeaders { + operationLocation?: string; +} + +/** + * Defines values for Language. + */ +export type Language = "en" | "es"; +/** + * Defines values for ModelStatus. + */ +export type ModelStatus = "creating" | "ready" | "invalid"; +/** + * Defines values for TrainStatus. + */ +export type TrainStatus = "succeeded" | "partiallySucceeded" | "failed"; +/** + * Defines values for ContentType. + */ +export type ContentType = + | "application/pdf" + | "image/jpeg" + | "image/png" + | "image/tiff"; +/** + * Defines values for OperationStatus. + */ +export type OperationStatus = "notStarted" | "running" | "succeeded" | "failed"; +/** + * Defines values for LengthUnit. + */ +export type LengthUnit = "pixel" | "inch"; +/** + * Defines values for FieldValueType. + */ +export type FieldValueType = + | "string" + | "date" + | "time" + | "phoneNumber" + | "number" + | "integer" + | "array" + | "object"; + +/** + * Contains response data for the trainCustomModelAsync operation. + */ +export type FormRecognizerClientTrainCustomModelAsyncResponse = FormRecognizerClientTrainCustomModelAsyncHeaders & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The parsed HTTP response headers. + */ + parsedHeaders: FormRecognizerClientTrainCustomModelAsyncHeaders; + }; +}; + +/** + * Optional parameters. + */ +export interface FormRecognizerClientGetCustomModelOptionalParams + extends coreHttp.OperationOptions { + /** + * Include list of extracted keys in model information. + */ + includeKeys?: boolean; +} + +/** + * Contains response data for the getCustomModel operation. + */ +export type FormRecognizerClientGetCustomModelResponse = Model & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: Model; + }; +}; + +/** + * Optional parameters. + */ +export interface FormRecognizerClientAnalyzeWithCustomModel$binaryOptionalParams + extends coreHttp.OperationOptions { + /** + * Upload file type + */ + contentType?: ContentType; + /** + * .json, .pdf, .jpg, .png or .tiff type file stream. + */ + fileStream?: coreHttp.HttpRequestBody; +} + +/** + * Optional parameters. + */ +export interface FormRecognizerClientAnalyzeWithCustomModel$jsonOptionalParams + extends coreHttp.OperationOptions { + /** + * .json, .pdf, .jpg, .png or .tiff type file stream. + */ + fileStream?: SourcePath; +} + +/** + * Optional parameters. + */ +export type FormRecognizerClientAnalyzeWithCustomModelOptionalParams = + | FormRecognizerClientAnalyzeWithCustomModel$binaryOptionalParams + | FormRecognizerClientAnalyzeWithCustomModel$jsonOptionalParams; + +/** + * Contains response data for the analyzeWithCustomModel operation. + */ +export type FormRecognizerClientAnalyzeWithCustomModelResponse = FormRecognizerClientAnalyzeWithCustomModelHeaders & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The parsed HTTP response headers. + */ + parsedHeaders: FormRecognizerClientAnalyzeWithCustomModelHeaders; + }; +}; + +/** + * Contains response data for the getAnalyzeFormResult operation. + */ +export type FormRecognizerClientGetAnalyzeFormResultResponse = AnalyzeOperationResult & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: AnalyzeOperationResult; + }; +}; + +/** + * Optional parameters. + */ +export interface FormRecognizerClientAnalyzeReceiptAsync$binaryOptionalParams + extends coreHttp.OperationOptions { + /** + * Upload file type + */ + contentType?: ContentType; + /** + * .json, .pdf, .jpg, .png or .tiff type file stream. + */ + fileStream?: coreHttp.HttpRequestBody; +} + +/** + * Optional parameters. + */ +export interface FormRecognizerClientAnalyzeReceiptAsync$jsonOptionalParams + extends coreHttp.OperationOptions { + /** + * .json, .pdf, .jpg, .png or .tiff type file stream. + */ + fileStream?: SourcePath; +} + +/** + * Optional parameters. + */ +export type FormRecognizerClientAnalyzeReceiptAsyncOptionalParams = + | FormRecognizerClientAnalyzeReceiptAsync$binaryOptionalParams + | FormRecognizerClientAnalyzeReceiptAsync$jsonOptionalParams; + +/** + * Contains response data for the analyzeReceiptAsync operation. + */ +export type FormRecognizerClientAnalyzeReceiptAsyncResponse = FormRecognizerClientAnalyzeReceiptAsyncHeaders & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The parsed HTTP response headers. + */ + parsedHeaders: FormRecognizerClientAnalyzeReceiptAsyncHeaders; + }; +}; + +/** + * Contains response data for the getAnalyzeReceiptResult operation. + */ +export type FormRecognizerClientGetAnalyzeReceiptResultResponse = AnalyzeOperationResult & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: AnalyzeOperationResult; + }; +}; + +/** + * Optional parameters. + */ +export interface FormRecognizerClientAnalyzeLayoutAsync$binaryOptionalParams + extends coreHttp.OperationOptions { + /** + * Upload file type + */ + contentType?: ContentType; + /** + * .json, .pdf, .jpg, .png or .tiff type file stream. + */ + fileStream?: coreHttp.HttpRequestBody; +} + +/** + * Optional parameters. + */ +export interface FormRecognizerClientAnalyzeLayoutAsync$jsonOptionalParams + extends coreHttp.OperationOptions { + /** + * .json, .pdf, .jpg, .png or .tiff type file stream. + */ + fileStream?: SourcePath; +} + +/** + * Optional parameters. + */ +export type FormRecognizerClientAnalyzeLayoutAsyncOptionalParams = + | FormRecognizerClientAnalyzeLayoutAsync$binaryOptionalParams + | FormRecognizerClientAnalyzeLayoutAsync$jsonOptionalParams; + +/** + * Contains response data for the analyzeLayoutAsync operation. + */ +export type FormRecognizerClientAnalyzeLayoutAsyncResponse = FormRecognizerClientAnalyzeLayoutAsyncHeaders & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The parsed HTTP response headers. + */ + parsedHeaders: FormRecognizerClientAnalyzeLayoutAsyncHeaders; + }; +}; + +/** + * Contains response data for the getAnalyzeLayoutResult operation. + */ +export type FormRecognizerClientGetAnalyzeLayoutResultResponse = AnalyzeOperationResult & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: AnalyzeOperationResult; + }; +}; + +/** + * Contains response data for the listCustomModels operation. + */ +export type FormRecognizerClientListCustomModelsResponse = Models & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: Models; + }; +}; + +/** + * Contains response data for the getCustomModels operation. + */ +export type FormRecognizerClientGetCustomModelsResponse = Models & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: Models; + }; +}; + +/** + * Contains response data for the listCustomModelsNext operation. + */ +export type FormRecognizerClientListCustomModelsNextResponse = Models & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: Models; + }; +}; + +/** + * Optional parameters. + */ +export interface FormRecognizerClientOptionalParams + extends coreHttp.ServiceClientOptions { + /** + * Overrides client endpoint. + */ + endpoint?: string; +} diff --git a/sdk/formrecognizer/ai-form-recognizer/src/generated/models/mappers.ts b/sdk/formrecognizer/ai-form-recognizer/src/generated/models/mappers.ts new file mode 100644 index 000000000000..bbfb4a7ee9fb --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/generated/models/mappers.ts @@ -0,0 +1,890 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is regenerated. + */ + +import * as coreHttp from "@azure/core-http"; + +export const TrainRequest: coreHttp.CompositeMapper = { + serializedName: "TrainRequest", + type: { + name: "Composite", + className: "TrainRequest", + modelProperties: { + source: { + type: { name: "String" }, + serializedName: "source", + required: true, + constraints: { MaxLength: 2048 } + }, + sourceFilter: { + serializedName: "sourceFilter", + type: { name: "Composite", className: "TrainSourceFilter" } + }, + useLabelFile: { + type: { name: "Boolean" }, + serializedName: "useLabelFile" + } + } + } +}; + +export const TrainSourceFilter: coreHttp.CompositeMapper = { + serializedName: "TrainSourceFilter", + type: { + name: "Composite", + className: "TrainSourceFilter", + modelProperties: { + prefix: { + type: { name: "String" }, + serializedName: "prefix", + constraints: { MaxLength: 1024 } + }, + includeSubFolders: { + type: { name: "Boolean" }, + serializedName: "includeSubFolders" + } + } + } +}; + +export const ErrorResponse: coreHttp.CompositeMapper = { + serializedName: "ErrorResponse", + type: { + name: "Composite", + className: "ErrorResponse", + modelProperties: { + error: { + serializedName: "error", + type: { name: "Composite", className: "ErrorInformation" } + } + } + } +}; + +export const ErrorInformation: coreHttp.CompositeMapper = { + serializedName: "ErrorInformation", + type: { + name: "Composite", + className: "ErrorInformation", + modelProperties: { + code: { + type: { name: "String" }, + serializedName: "code", + required: true + }, + message: { + type: { name: "String" }, + serializedName: "message", + required: true + } + } + } +}; + +export const Model: coreHttp.CompositeMapper = { + serializedName: "Model", + type: { + name: "Composite", + className: "Model", + modelProperties: { + modelInfo: { + serializedName: "modelInfo", + type: { name: "Composite", className: "ModelInfo" } + }, + keys: { + serializedName: "keys", + type: { name: "Composite", className: "KeysResult" } + }, + trainResult: { + serializedName: "trainResult", + type: { name: "Composite", className: "TrainResult" } + } + } + } +}; + +export const ModelInfo: coreHttp.CompositeMapper = { + serializedName: "ModelInfo", + type: { + name: "Composite", + className: "ModelInfo", + modelProperties: { + modelId: { + type: { name: "Uuid" }, + serializedName: "modelId", + required: true + }, + status: { + type: { name: "Enum", allowedValues: ["creating", "ready", "invalid"] }, + serializedName: "status", + required: true + }, + createdOn: { + type: { name: "DateTime" }, + serializedName: "createdDateTime", + required: true + }, + lastUpdatedOn: { + type: { name: "DateTime" }, + serializedName: "lastUpdatedDateTime", + required: true + } + } + } +}; + +export const KeysResult: coreHttp.CompositeMapper = { + serializedName: "KeysResult", + type: { + name: "Composite", + className: "KeysResult", + modelProperties: { + clusters: { + type: { + name: "Dictionary", + value: { + type: { + name: "Sequence", + element: { + type: { name: "String" }, + serializedName: "KeysResultClustersItemsItem" + } + }, + serializedName: "ArrayOfKeysResultClustersItemsItem", + constraints: { UniqueItems: true } + } + }, + serializedName: "clusters", + required: true + } + } + } +}; + +export const TrainResult: coreHttp.CompositeMapper = { + serializedName: "TrainResult", + type: { + name: "Composite", + className: "TrainResult", + modelProperties: { + trainingDocuments: { + type: { + name: "Sequence", + element: { + type: { name: "Composite", className: "TrainingDocumentInfo" } + } + }, + serializedName: "trainingDocuments", + required: true + }, + fields: { + type: { + name: "Sequence", + element: { + type: { name: "Composite", className: "FormFieldsReport" } + } + }, + serializedName: "fields" + }, + averageModelAccuracy: { + type: { name: "Number" }, + serializedName: "averageModelAccuracy" + }, + errors: { + type: { + name: "Sequence", + element: { + type: { name: "Composite", className: "ErrorInformation" } + } + }, + serializedName: "errors" + } + } + } +}; + +export const TrainingDocumentInfo: coreHttp.CompositeMapper = { + serializedName: "TrainingDocumentInfo", + type: { + name: "Composite", + className: "TrainingDocumentInfo", + modelProperties: { + documentName: { + type: { name: "String" }, + serializedName: "documentName", + required: true + }, + pages: { + type: { name: "Number" }, + serializedName: "pages", + required: true + }, + errors: { + type: { + name: "Sequence", + element: { + type: { name: "Composite", className: "ErrorInformation" } + } + }, + serializedName: "errors", + required: true + }, + status: { + type: { + name: "Enum", + allowedValues: ["succeeded", "partiallySucceeded", "failed"] + }, + serializedName: "status", + required: true + } + } + } +}; + +export const FormFieldsReport: coreHttp.CompositeMapper = { + serializedName: "FormFieldsReport", + type: { + name: "Composite", + className: "FormFieldsReport", + modelProperties: { + fieldName: { + type: { name: "String" }, + serializedName: "fieldName", + required: true + }, + accuracy: { + type: { name: "Number" }, + serializedName: "accuracy", + required: true + } + } + } +}; + +export const SourcePath: coreHttp.CompositeMapper = { + serializedName: "SourcePath", + type: { + name: "Composite", + className: "SourcePath", + modelProperties: { + source: { + type: { name: "String" }, + serializedName: "source", + constraints: { MaxLength: 2048 } + } + } + } +}; + +export const AnalyzeOperationResult: coreHttp.CompositeMapper = { + serializedName: "AnalyzeOperationResult", + type: { + name: "Composite", + className: "AnalyzeOperationResult", + modelProperties: { + status: { + type: { + name: "Enum", + allowedValues: ["notStarted", "running", "succeeded", "failed"] + }, + serializedName: "status", + required: true + }, + createdOn: { + type: { name: "DateTime" }, + serializedName: "createdDateTime", + required: true + }, + lastUpdatedOn: { + type: { name: "DateTime" }, + serializedName: "lastUpdatedDateTime", + required: true + }, + analyzeResult: { + serializedName: "analyzeResult", + type: { name: "Composite", className: "AnalyzeResult" } + } + } + } +}; + +export const AnalyzeResult: coreHttp.CompositeMapper = { + serializedName: "AnalyzeResult", + type: { + name: "Composite", + className: "AnalyzeResult", + modelProperties: { + version: { + type: { name: "String" }, + serializedName: "version", + required: true + }, + readResults: { + type: { + name: "Sequence", + element: { type: { name: "Composite", className: "ReadResult" } } + }, + serializedName: "readResults", + required: true + }, + pageResults: { + type: { + name: "Sequence", + element: { type: { name: "Composite", className: "PageResult" } } + }, + serializedName: "pageResults" + }, + documentResults: { + type: { + name: "Sequence", + element: { type: { name: "Composite", className: "DocumentResult" } } + }, + serializedName: "documentResults" + }, + errors: { + type: { + name: "Sequence", + element: { + type: { name: "Composite", className: "ErrorInformation" } + } + }, + serializedName: "errors" + } + } + } +}; + +export const ReadResult: coreHttp.CompositeMapper = { + serializedName: "ReadResult", + type: { + name: "Composite", + className: "ReadResult", + modelProperties: { + pageNumber: { + type: { name: "Number" }, + serializedName: "page", + required: true, + constraints: { InclusiveMinimum: 1 } + }, + angle: { + type: { name: "Number" }, + serializedName: "angle", + required: true, + constraints: { InclusiveMaximum: 180, InclusiveMinimum: -180 } + }, + width: { + type: { name: "Number" }, + serializedName: "width", + required: true, + constraints: {} + }, + height: { + type: { name: "Number" }, + serializedName: "height", + required: true, + constraints: {} + }, + unit: { + type: { name: "Enum", allowedValues: ["pixel", "inch"] }, + serializedName: "unit", + required: true + }, + language: { type: { name: "String" }, serializedName: "language" }, + lines: { + type: { + name: "Sequence", + element: { type: { name: "Composite", className: "TextLine" } } + }, + serializedName: "lines" + } + } + } +}; + +export const TextLine: coreHttp.CompositeMapper = { + serializedName: "TextLine", + type: { + name: "Composite", + className: "TextLine", + modelProperties: { + text: { + type: { name: "String" }, + serializedName: "text", + required: true + }, + boundingBox: { + type: { + name: "Sequence", + element: { + type: { name: "Number" }, + serializedName: "BoundingBoxItem" + } + }, + serializedName: "boundingBox", + required: true, + constraints: { MinItems: 8, MaxItems: 8 } + }, + language: { type: { name: "String" }, serializedName: "language" }, + words: { + type: { + name: "Sequence", + element: { type: { name: "Composite", className: "TextWord" } } + }, + serializedName: "words", + required: true + } + } + } +}; + +export const TextWord: coreHttp.CompositeMapper = { + serializedName: "TextWord", + type: { + name: "Composite", + className: "TextWord", + modelProperties: { + text: { + type: { name: "String" }, + serializedName: "text", + required: true + }, + boundingBox: { + type: { + name: "Sequence", + element: { + type: { name: "Number" }, + serializedName: "BoundingBoxItem" + } + }, + serializedName: "boundingBox", + required: true, + constraints: { MinItems: 8, MaxItems: 8 } + }, + confidence: { + type: { name: "Number" }, + serializedName: "confidence", + constraints: { InclusiveMaximum: 1 } + } + } + } +}; + +export const PageResult: coreHttp.CompositeMapper = { + serializedName: "PageResult", + type: { + name: "Composite", + className: "PageResult", + modelProperties: { + pageNumber: { + type: { name: "Number" }, + serializedName: "page", + required: true, + constraints: { InclusiveMinimum: 1 } + }, + clusterId: { + type: { name: "Number" }, + serializedName: "clusterId", + constraints: {} + }, + keyValuePairs: { + type: { + name: "Sequence", + element: { type: { name: "Composite", className: "KeyValuePair" } } + }, + serializedName: "keyValuePairs" + }, + tables: { + type: { + name: "Sequence", + element: { type: { name: "Composite", className: "DataTable" } } + }, + serializedName: "tables" + } + } + } +}; + +export const KeyValuePair: coreHttp.CompositeMapper = { + serializedName: "KeyValuePair", + type: { + name: "Composite", + className: "KeyValuePair", + modelProperties: { + label: { type: { name: "String" }, serializedName: "label" }, + key: { + serializedName: "key", + type: { name: "Composite", className: "KeyValueElement" } + }, + value: { + serializedName: "value", + type: { name: "Composite", className: "KeyValueElement" } + }, + confidence: { + type: { name: "Number" }, + serializedName: "confidence", + required: true, + constraints: { InclusiveMaximum: 1 } + } + } + } +}; + +export const KeyValueElement: coreHttp.CompositeMapper = { + serializedName: "KeyValueElement", + type: { + name: "Composite", + className: "KeyValueElement", + modelProperties: { + text: { + type: { name: "String" }, + serializedName: "text", + required: true + }, + boundingBox: { + type: { + name: "Sequence", + element: { + type: { name: "Number" }, + serializedName: "BoundingBoxItem" + } + }, + serializedName: "boundingBox", + constraints: { MinItems: 8, MaxItems: 8 } + }, + elements: { + type: { + name: "Sequence", + element: { + type: { name: "String" }, + serializedName: "ElementReference" + } + }, + serializedName: "elements" + } + } + } +}; + +export const DataTable: coreHttp.CompositeMapper = { + serializedName: "DataTable", + type: { + name: "Composite", + className: "DataTable", + modelProperties: { + rows: { + type: { name: "Number" }, + serializedName: "rows", + required: true, + constraints: { InclusiveMinimum: 1 } + }, + columns: { + type: { name: "Number" }, + serializedName: "columns", + required: true, + constraints: { InclusiveMinimum: 1 } + }, + cells: { + type: { + name: "Sequence", + element: { type: { name: "Composite", className: "DataTableCell" } } + }, + serializedName: "cells", + required: true + } + } + } +}; + +export const DataTableCell: coreHttp.CompositeMapper = { + serializedName: "DataTableCell", + type: { + name: "Composite", + className: "DataTableCell", + modelProperties: { + rowIndex: { + type: { name: "Number" }, + serializedName: "rowIndex", + required: true, + constraints: {} + }, + columnIndex: { + type: { name: "Number" }, + serializedName: "columnIndex", + required: true, + constraints: {} + }, + rowSpan: { + type: { name: "Number" }, + serializedName: "rowSpan", + defaultValue: 1, + constraints: { InclusiveMinimum: 1 } + }, + columnSpan: { + type: { name: "Number" }, + serializedName: "columnSpan", + defaultValue: 1, + constraints: { InclusiveMinimum: 1 } + }, + text: { + type: { name: "String" }, + serializedName: "text", + required: true + }, + boundingBox: { + type: { + name: "Sequence", + element: { + type: { name: "Number" }, + serializedName: "BoundingBoxItem" + } + }, + serializedName: "boundingBox", + required: true, + constraints: { MinItems: 8, MaxItems: 8 } + }, + confidence: { + type: { name: "Number" }, + serializedName: "confidence", + required: true, + constraints: { InclusiveMaximum: 1 } + }, + elements: { + type: { + name: "Sequence", + element: { + type: { name: "String" }, + serializedName: "ElementReference" + } + }, + serializedName: "elements" + }, + isHeader: { type: { name: "Boolean" }, serializedName: "isHeader" }, + isFooter: { type: { name: "Boolean" }, serializedName: "isFooter" } + } + } +}; + +export const DocumentResult: coreHttp.CompositeMapper = { + serializedName: "DocumentResult", + type: { + name: "Composite", + className: "DocumentResult", + modelProperties: { + docType: { + type: { name: "String" }, + serializedName: "docType", + required: true + }, + pageRange: { + type: { + name: "Sequence", + element: { + type: { name: "Number" }, + serializedName: "ArrayItemschema", + constraints: { InclusiveMinimum: 1 } + } + }, + serializedName: "pageRange", + required: true, + constraints: { MinItems: 2, MaxItems: 2 } + }, + fields: { + type: { + name: "Dictionary", + value: { type: { name: "Composite", className: "FieldValue" } } + }, + serializedName: "fields", + required: true + } + } + } +}; + +export const FieldValue: coreHttp.CompositeMapper = { + serializedName: "FieldValue", + type: { + name: "Composite", + className: "FieldValue", + modelProperties: { + type: { + type: { + name: "Enum", + allowedValues: [ + "string", + "date", + "time", + "phoneNumber", + "number", + "integer", + "array", + "object" + ] + }, + serializedName: "type", + required: true + }, + valueString: { type: { name: "String" }, serializedName: "valueString" }, + valueDate: { type: { name: "Date" }, serializedName: "valueDate" }, + valueTime: { type: { name: "String" }, serializedName: "valueTime" }, + valuePhoneNumber: { + type: { name: "String" }, + serializedName: "valuePhoneNumber" + }, + valueNumber: { type: { name: "Number" }, serializedName: "valueNumber" }, + valueInteger: { + type: { name: "Number" }, + serializedName: "valueInteger" + }, + valueArray: { + type: { + name: "Sequence", + element: { type: { name: "Composite", className: "FieldValue" } } + }, + serializedName: "valueArray" + }, + valueObject: { + type: { + name: "Dictionary", + value: { type: { name: "Composite", className: "FieldValue" } } + }, + serializedName: "valueObject" + }, + text: { type: { name: "String" }, serializedName: "text" }, + boundingBox: { + type: { + name: "Sequence", + element: { + type: { name: "Number" }, + serializedName: "BoundingBoxItem" + } + }, + serializedName: "boundingBox", + constraints: { MinItems: 8, MaxItems: 8 } + }, + confidence: { + type: { name: "Number" }, + serializedName: "confidence", + constraints: { InclusiveMaximum: 1 } + }, + elements: { + type: { + name: "Sequence", + element: { + type: { name: "String" }, + serializedName: "ElementReference" + } + }, + serializedName: "elements" + }, + pageNumber: { + type: { name: "Number" }, + serializedName: "page", + constraints: { InclusiveMinimum: 1 } + } + } + } +}; + +export const Models: coreHttp.CompositeMapper = { + serializedName: "Models", + type: { + name: "Composite", + className: "Models", + modelProperties: { + summary: { + serializedName: "summary", + type: { name: "Composite", className: "ModelsSummary" } + }, + modelList: { + type: { + name: "Sequence", + element: { type: { name: "Composite", className: "ModelInfo" } } + }, + serializedName: "modelList" + }, + nextLink: { type: { name: "String" }, serializedName: "nextLink" } + } + } +}; + +export const ModelsSummary: coreHttp.CompositeMapper = { + serializedName: "ModelsSummary", + type: { + name: "Composite", + className: "ModelsSummary", + modelProperties: { + count: { + type: { name: "Number" }, + serializedName: "count", + required: true + }, + limit: { + type: { name: "Number" }, + serializedName: "limit", + required: true + }, + lastUpdatedOn: { + type: { name: "DateTime" }, + serializedName: "lastUpdatedDateTime", + required: true + } + } + } +}; + +export const FormRecognizerClientTrainCustomModelAsyncHeaders: coreHttp.CompositeMapper = { + serializedName: "formRecognizerClient_trainCustomModelAsyncHeaders", + type: { + name: "Composite", + className: "FormRecognizerClientTrainCustomModelAsyncHeaders", + modelProperties: { + location: { type: { name: "String" }, serializedName: "location" } + } + } +}; + +export const FormRecognizerClientAnalyzeWithCustomModelHeaders: coreHttp.CompositeMapper = { + serializedName: "formRecognizerClient_analyzeWithCustomModelHeaders", + type: { + name: "Composite", + className: "FormRecognizerClientAnalyzeWithCustomModelHeaders", + modelProperties: { + operationLocation: { + type: { name: "String" }, + serializedName: "operation-location" + } + } + } +}; + +export const FormRecognizerClientAnalyzeReceiptAsyncHeaders: coreHttp.CompositeMapper = { + serializedName: "formRecognizerClient_analyzeReceiptAsyncHeaders", + type: { + name: "Composite", + className: "FormRecognizerClientAnalyzeReceiptAsyncHeaders", + modelProperties: { + operationLocation: { + type: { name: "String" }, + serializedName: "operation-location" + } + } + } +}; + +export const FormRecognizerClientAnalyzeLayoutAsyncHeaders: coreHttp.CompositeMapper = { + serializedName: "formRecognizerClient_analyzeLayoutAsyncHeaders", + type: { + name: "Composite", + className: "FormRecognizerClientAnalyzeLayoutAsyncHeaders", + modelProperties: { + operationLocation: { + type: { name: "String" }, + serializedName: "operation-location" + } + } + } +}; diff --git a/sdk/formrecognizer/ai-form-recognizer/src/generated/models/parameters.ts b/sdk/formrecognizer/ai-form-recognizer/src/generated/models/parameters.ts new file mode 100644 index 000000000000..ac7f8b142613 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/generated/models/parameters.ts @@ -0,0 +1,136 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is regenerated. + */ + +import * as coreHttp from "@azure/core-http"; +import * as Mappers from "../models/mappers"; + +export const trainRequest: coreHttp.OperationParameter = { + parameterPath: "trainRequest", + mapper: Mappers.TrainRequest +}; + +export const endpoint: coreHttp.OperationURLParameter = { + parameterPath: "endpoint", + mapper: { + serializedName: "endpoint", + required: true, + type: { + name: "String" + } + }, + skipEncoding: true +}; + +export const modelId: coreHttp.OperationURLParameter = { + parameterPath: "modelId", + mapper: { + serializedName: "modelId", + required: true, + type: { + name: "Uuid" + } + } +}; + +export const includeKeys: coreHttp.OperationQueryParameter = { + parameterPath: ["options", "includeKeys"], + mapper: { + serializedName: "includeKeys", + type: { + name: "Boolean" + } + } +}; + +export const contentType: coreHttp.OperationParameter = { + parameterPath: ["options", "contentType"], + mapper: { + serializedName: "Content-Type", + type: { + name: "Enum", + allowedValues: [ + "application/pdf", + "image/jpeg", + "image/png", + "image/tiff" + ] + } + } +}; + +export const fileStream: coreHttp.OperationParameter = { + parameterPath: ["options", "fileStream"], + mapper: { + serializedName: "fileStream", + type: { + name: "Stream" + } + } +}; + +export const fileStream1: coreHttp.OperationParameter = { + parameterPath: ["options", "fileStream"], + mapper: Mappers.SourcePath +}; + +export const includeTextDetails: coreHttp.OperationQueryParameter = { + parameterPath: ["options", "includeTextDetails"], + mapper: { + serializedName: "includeTextDetails", + type: { + name: "Boolean" + } + } +}; + +export const resultId: coreHttp.OperationURLParameter = { + parameterPath: "resultId", + mapper: { + serializedName: "resultId", + required: true, + type: { + name: "Uuid" + } + } +}; + +export const op: coreHttp.OperationQueryParameter = { + parameterPath: "op", + mapper: { + defaultValue: "full", + serializedName: "op", + isConstant: true, + type: { + name: "String" + } + } +}; + +export const op1: coreHttp.OperationQueryParameter = { + parameterPath: "op", + mapper: { + defaultValue: "summary", + serializedName: "op", + isConstant: true, + type: { + name: "String" + } + } +}; + +export const nextLink: coreHttp.OperationURLParameter = { + parameterPath: "nextLink", + mapper: { + serializedName: "nextLink", + required: true, + type: { + name: "String" + } + }, + skipEncoding: true +}; diff --git a/sdk/formrecognizer/ai-form-recognizer/src/index.ts b/sdk/formrecognizer/ai-form-recognizer/src/index.ts new file mode 100644 index 000000000000..3739768f7e43 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/index.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export { FormRecognizerClientOptions, FormRecognizerOperationOptions } from "./common"; +export * from "./formRecognizerClient"; +export * from "./formTrainingClient"; +export { FormRecognizerApiKeyCredential } from "./formRecognizerApiKeyCredential"; + +export * from "./models"; diff --git a/sdk/formrecognizer/ai-form-recognizer/src/logger.ts b/sdk/formrecognizer/ai-form-recognizer/src/logger.ts new file mode 100644 index 000000000000..b27347019c50 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/logger.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { createClientLogger } from "@azure/logger"; + +/** + * The @azure/logger configuration for this package. + */ +export const logger = createClientLogger("ai-form-recognizer"); diff --git a/sdk/formrecognizer/ai-form-recognizer/src/lro/analyze/poller.ts b/sdk/formrecognizer/ai-form-recognizer/src/lro/analyze/poller.ts new file mode 100644 index 000000000000..43cb04e383d6 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/lro/analyze/poller.ts @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { delay, AbortSignalLike } from "@azure/core-http"; +import { Poller, PollOperation, PollOperationState } from "@azure/core-lro"; +import { RecognizeFormsOptions, RecognizeContentOptions, RecognizeReceiptsOptions } from "../../formRecognizerClient"; + +import { OperationStatus, ContentType } from "../../generated/models"; +import { FormRecognizerRequestBody } from "../../models"; +export { OperationStatus }; + +export type RecognizeOptions = RecognizeReceiptsOptions | RecognizeContentOptions | RecognizeFormsOptions; + +export interface PollerOperationOptions { + /** + * Time between each polling in milliseconds. + */ + intervalInMs?: number; + /** + * callback to receive events on the progress of download operation. + */ + onProgress?: (state: BeginRecognizePollState) => void; + /** + * A serialized poller, used to resume an existing operation + */ + resumeFrom?: string; +} + +/** + * Defines the operations from a analyze client that are needed for the poller + * to work + */ +export type RecognizePollerClient = { + // returns a result id to retrieve results + beginRecognize: ( + source: FormRecognizerRequestBody, + contentType?: ContentType, + analyzeOptions?: RecognizeOptions, + modelId?: string + ) => Promise<{ operationLocation?: string }>; + // retrieves analyze result + getRecognizeResult: (resultId: string, options: { abortSignal?: AbortSignalLike }) => Promise; +}; + +export interface BeginRecognizePollState extends PollOperationState { + readonly client: RecognizePollerClient; + source?: FormRecognizerRequestBody; + contentType?: ContentType; + modelId?: string; + resultId?: string; + status: OperationStatus; + readonly analyzeOptions?: RecognizeOptions; +} + +export interface BeginRecognizePollerOperation + extends PollOperation, T> {} + +/** + * @internal + */ +export type BeginRecognizePollerOptions = { + client: RecognizePollerClient; + source: FormRecognizerRequestBody; + contentType?: ContentType; + modelId?: string; + intervalInMs?: number; + resultId?: string; + onProgress?: (state: BeginRecognizePollState) => void; + resumeFrom?: string; +} & RecognizeOptions; + +/** + * Class that represents a poller that waits until a model has been trained. + */ +export class BeginRecognizePoller extends Poller< + BeginRecognizePollState, + T +> { + public intervalInMs: number; + + constructor(options: BeginRecognizePollerOptions) { + const { + client, + source, + contentType, + intervalInMs = 5000, + resultId, + modelId, + onProgress, + resumeFrom + } = options; + + let state: BeginRecognizePollState | undefined; + + if (resumeFrom) { + state = JSON.parse(resumeFrom).state; + } + + const operation = makeBeginRecognizePollOperation({ + ...state, + client, + source, + contentType, + resultId, + modelId, + status: "notStarted", + analyzeOptions: options + }); + + super(operation); + + if (typeof onProgress === "function") { + this.onProgress(onProgress); + } + + this.intervalInMs = intervalInMs; + } + + public delay(): Promise { + return delay(this.intervalInMs); + } +} +/** + * Creates a poll operation given the provided state. + * @ignore + */ +function makeBeginRecognizePollOperation( + state: BeginRecognizePollState +): BeginRecognizePollerOperation { + return { + state: { ...state }, + + async cancel(_options = {}): Promise> { + throw new Error("Cancel operation is not supported."); + }, + + async update(options = {}): Promise> { + const state = this.state; + const { client, source, contentType, analyzeOptions, modelId } = state; + + if (!state.isStarted) { + if (!source) { + throw new Error("Expect a valid 'source'"); + } + + state.isStarted = true; + const result = await client.beginRecognize( + source, + contentType, + analyzeOptions || {}, + modelId + ); + if (!result.operationLocation) { + throw new Error("Expect a valid 'operationLocation' to retrieve analyze results"); + } + const lastSlashIndex = result.operationLocation.lastIndexOf("/"); + state.resultId = result.operationLocation.substring(lastSlashIndex + 1); + // source is no longer needed + state.source = undefined; + } + + const response = await client.getRecognizeResult(state.resultId!, { + abortSignal: analyzeOptions?.abortSignal + }); + + state.status = response.status; + if (!state.isCompleted) { + if (response.status === "running" && typeof options.fireProgress === "function") { + options.fireProgress(state); + } else if (response.status === "succeeded") { + state.result = response; + state.isCompleted = true; + } else if (response.status === "failed") { + state.error = new Error(`Model training failed with invalid model status.`); + state.isCompleted = true; + } + } + + return makeBeginRecognizePollOperation(state); + }, + + toString() { + return JSON.stringify({ state: this.state }, (key, value) => { + if (key === "client" || key === "source") { + return undefined; + } + return value; + }); + } + }; +} diff --git a/sdk/formrecognizer/ai-form-recognizer/src/lro/train/poller.ts b/sdk/formrecognizer/ai-form-recognizer/src/lro/train/poller.ts new file mode 100644 index 000000000000..c66bb0ea9cfe --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/lro/train/poller.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { delay } from "@azure/core-http"; +import { Poller, PollOperation, PollOperationState } from "@azure/core-lro"; +import { TrainModelOptions, GetModelOptions } from "../../formTrainingClient"; + +import { + ModelStatus, + FormRecognizerClientTrainCustomModelAsyncResponse as TrainCustomModelAsyncResponse +} from "../../generated/models"; +export { ModelStatus, TrainCustomModelAsyncResponse }; + +/** + * Defines the operations from a {@link FormRecognizerClient} that are needed for the poller + * returned by {@link FormRecognizerClient.beginTraining} to work. + */ +export type TrainPollerClient = { + getModel: (modelId: string, options: GetModelOptions) => Promise; + trainCustomModelInternal: ( + source: string, + useLabelFile?: boolean, + options?: TrainModelOptions + ) => Promise<{ location?: string }>; +}; + +export interface BeginTrainingPollState extends PollOperationState { + readonly client: TrainPollerClient; + source: string; + modelId?: string; + status: ModelStatus; + readonly trainModelOptions?: TrainModelOptions; +} + +export interface BeginTrainingPollerOperation + extends PollOperation, T> {} + +/** + * @internal + */ +export interface BeginTrainingPollerOptions { + client: TrainPollerClient; + source: string; + intervalInMs?: number; + onProgress?: (state: BeginTrainingPollState) => void; + resumeFrom?: string; + trainModelOptions?: TrainModelOptions; +} + +/** + * Class that represents a poller that waits until a model has been trained. + */ +export class BeginTrainingPoller extends Poller< + BeginTrainingPollState, + T +> { + public intervalInMs: number; + + constructor(options: BeginTrainingPollerOptions) { + const { + client, + source, + intervalInMs = 5000, + onProgress, + resumeFrom, + trainModelOptions + } = options; + + let state: BeginTrainingPollState | undefined; + + if (resumeFrom) { + state = JSON.parse(resumeFrom).state; + } + + const operation = makeBeginTrainingPollOperation({ + ...state, + client, + source, + status: "creating", + trainModelOptions + }); + + super(operation); + + if (typeof onProgress === "function") { + this.onProgress(onProgress); + } + + this.intervalInMs = intervalInMs; + } + + public delay(): Promise { + return delay(this.intervalInMs); + } +} + +/** + * Creates a poll operation given the provided state. + * @ignore + */ +function makeBeginTrainingPollOperation( + state: BeginTrainingPollState +): BeginTrainingPollerOperation { + return { + state: { ...state }, + + async cancel(_options = {}): Promise> { + throw new Error("Cancel operation is not supported."); + }, + + async update(options = {}): Promise> { + const state = this.state; + const { client, source, trainModelOptions } = state; + + if (!state.isStarted) { + state.isStarted = true; + const result = await client.trainCustomModelInternal( + source, + false, + trainModelOptions || {} + ); + if (!result.location) { + throw new Error("Expect a valid 'operationLocation' to retrieve analyze results"); + } + const lastSlashIndex = result.location.lastIndexOf("/"); + state.modelId = result.location.substring(lastSlashIndex + 1); + } + + const model = await client.getModel(state.modelId!, { + abortSignal: trainModelOptions?.abortSignal + }); + + state.status = model.modelInfo.status; + + if (!state.isCompleted) { + if (model.modelInfo.status === "creating" && typeof options.fireProgress === "function") { + options.fireProgress(state); + } else if (model.modelInfo.status === "ready") { + state.result = model; + state.isCompleted = true; + } else if (model.modelInfo.status === "invalid") { + state.error = new Error(`Model training failed with invalid model status.`); + state.result = model; + state.isCompleted = true; + } + } + + return makeBeginTrainingPollOperation(state); + }, + + toString() { + return JSON.stringify({ state: this.state }, (key, value) => { + if (key === "client") { + return undefined; + } + return value; + }); + } + }; +} diff --git a/sdk/formrecognizer/ai-form-recognizer/src/models.ts b/sdk/formrecognizer/ai-form-recognizer/src/models.ts new file mode 100644 index 000000000000..345e028dbd34 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/models.ts @@ -0,0 +1,983 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as coreHttp from "@azure/core-http"; + +import { + AnalyzeOperationResult as AnalyzeOperationResultModel, + ErrorInformation, + FormFieldsReport, + KeysResult, + KeyValueElement as KeyValueElementModel, + KeyValuePair as KeyValuePairModel, + Language, + LengthUnit, + Model, + Models, + ModelsSummary, + ModelStatus, + TrainingDocumentInfo, + TrainResult, + TrainStatus, + OperationStatus, + ModelInfo +} from "./generated/models"; + +export { + AnalyzeOperationResultModel, + ErrorInformation, + FormFieldsReport, + KeysResult, + KeyValueElementModel, + KeyValuePairModel, + Language, + LengthUnit, + Models, + ModelsSummary, + ModelStatus, + OperationStatus, + TrainingDocumentInfo, + TrainStatus, + TrainResult +}; + +/** + * Represents a point used to defined bounding boxes. The unit is either 'pixel' or 'inch' (See {link @LengthUnit}). + */ +export interface Point2D { + /** + * x coordinate + */ + x: number; + /** + * y coordinate + */ + y: number; +} + +/** + * Represents common properties of recognized form contents. + */ +export interface FormElementCommon { + /** + * The 1-based page number in the input document. + */ + pageNumber: number; + /** + * The text content of the word. + */ + text: string; + /** + * Bounding box of an recognized word. + */ + boundingBox: Point2D[]; +} + +/** + * Represents an recognized word. + */ +export interface FormWord extends FormElementCommon { + /** + * Element kind - "word" + */ + kind: "word"; + /** + * Confidence value. + */ + confidence?: number; + /** + * The recognized text line that contains this recognized word + */ + containingLine?: FormLine; +} + +/** + * Represents an recognized text line. + */ +export interface FormLine extends FormElementCommon { + /** + * Element kind - "line" + */ + kind: "line"; + /** + * The detected language of this line, if different from the overall page language. Possible + * values include: 'en', 'es' + */ + // language?: Language; + /** + * List of words in the text line. + */ + words: FormWord[]; +} + +/** + * Represents an recognized check box + */ +// export interface FormCheckBox extends FormContent { +// /** +// * Element kind - "checkbox" +// */ +// kind: "checkbox"; +// checked: boolean; +// } + +/** + * Information about an recognized element in the form. Examples include + * words, lines, checkbox, etc. + */ +export type FormElement = FormWord | FormLine; // | FormCheckBox; + +/** + * Represents a cell in recognized table + */ +export interface FormTableCell { + /** + * Row index of the cell. + */ + rowIndex: number; + /** + * Column index of the cell. + */ + columnIndex: number; + /** + * Number of rows spanned by this cell. + */ + rowSpan?: number; + /** + * Number of columns spanned by this cell. + */ + columnSpan?: number; + /** + * Text content of the cell. + */ + text: string; + /** + * Bounding box of the cell. + */ + boundingBox: Point2D[]; + /** + * Confidence value. + */ + confidence: number; + /** + * When includeTextDetails is set to true, a list of references to the text elements constituting this table cell. + */ + elements?: FormElement[]; + /** + * Is the current cell a header cell? + */ + isHeader?: boolean; + /** + * Is the current cell a footer cell? + */ + isFooter?: boolean; +} + +/** + * Represents a row of data table cells in recognized table. + */ +export interface FormTableRow { + /** + * List of data table cells in a {@link FormTableRow} + */ + cells: FormTableCell[]; +} + +/** + * Information about the recognized table contained in a page. + */ +export interface FormTable { + /** + * Number of rows in the data table + */ + rowCount: number; + /** + * Number of columns in the data table + */ + columnCount: number; + /** + * List of rows in the data table + */ + rows: FormTableRow[]; +} + +/** + * Represents recognized text elements of label-value pairs. + * For example, "Work Address" is the label of + * "Work Address: One Microsoft Way, Redmond, WA" + */ +export interface FormText { + /** + * The bounding box of the recognized label or value + */ + boundingBox?: Point2D[]; + /** + * When includeTextDetails is set to true, a list of references to the text elements constituting this name or value. + */ + elements?: FormElement[]; + /** + * The text content of the recognized label or value + */ + text: string; +} + +/** + * Represents recognized text elements in label-value pairs. + * For example, "Address": "One Microsoft Way, Redmond, WA" + */ +export interface FormField { + /** + * Confidence value. + */ + confidence: number; + /** + * Information about the recognized label in a label-value pair. + */ + fieldLabel: FormText; + /** + * A user defined label for the label-value pair entry. + */ + label?: string; + /** + * Information about the recognized value in a label/value pair. + */ + valueText: FormText; +} + +/** + * Recognized information from a single page. + */ +export interface RecognizedPage { + /** + * Page number + */ + pageNumber: number; + /** + * Id of recognized form type + */ + formTypeId?: number; + /** + * List of name/value pairs recognized from the page + */ + fields?: FormField[]; + /** + * List of data tables recognized form the page + */ + tables?: FormTable[]; +} + +/** + * Represents a Form page range + */ +export interface FormPageRange { + /** + * The page number of the first page in the range + */ + firstPageNumber: number; + /** + * The page number of the last page in the range + */ + lastPageNumber: number; +} + +/** + * Represent recognized forms consists of text fields that have semantic meanings. + */ +export interface RecognizedForm { + /** + * Document type. + */ + docType: string; + /** + * First and last page number where the document is found. + */ + pageRange: FormPageRange; + /** + * Dictionary of named field values. + */ + fields: { [propertyName: string]: FieldValue }; +} + +/** + * Properties common to the recognized text field + */ +export interface CommonFieldValue { + /** + * Text content of the recognized field. + */ + text?: string; + /** + * Bounding box of the field value, if appropriate. + */ + boundingBox?: Point2D[]; + /** + * Confidence score. + */ + confidence?: number; + /** + * When includeTextDetails is set to true, a list of references to the text elements constituting + * this field. + */ + elements?: FormElement[]; + /** + * The 1-based page number in the input document. + */ + pageNumber?: number; +} + +/** + * Represents a field of string value. + */ +export type StringFieldValue = { + type: "string"; + value?: string; +} & CommonFieldValue; + +/** + * Represents a date field + */ +export type DateFieldValue = { + type: "date"; + value?: Date; +} & CommonFieldValue; + +/** + * Represent a time field + */ +export type TimeFieldValue = { + type: "time"; + value?: string; +} & CommonFieldValue; + +/** + * Represents a phone number field + */ +export type PhoneNumberFieldValue = { + type: "phoneNumber"; + value?: string; +} & CommonFieldValue; + +/** + * Represents a number field + */ +export type NumberFieldValue = { + type: "number"; + value?: number; +} & CommonFieldValue; + +/** + * Represents an integer field + */ +export type IntegerFieldValue = { + type: "integer"; + value?: number; +} & CommonFieldValue; + +/** + * Represents a special field that contains an array of fields + */ +export interface ArrayFieldValue { + type: "array"; + value?: FieldValue[]; +} + +/** + * Represents a special field that contains other fields as its properties + */ +export interface ObjectFieldValue { + type: "object"; + value?: { [propertyName: string]: FieldValue }; +} + +/** + * Union type of all field types + */ +export type FieldValue = + | StringFieldValue + | DateFieldValue + | TimeFieldValue + | PhoneNumberFieldValue + | NumberFieldValue + | IntegerFieldValue + | ArrayFieldValue + | ObjectFieldValue; + +/** + * Represents an recognized item field in a receipt. + */ +export type ReceiptItemField = { + type: "object"; + value: { + Name: StringFieldValue; + Quantity: NumberFieldValue; + Price: NumberFieldValue; + TotalPrice: NumberFieldValue; + }; +} & CommonFieldValue; + +/** + * The values in an recognized receipt item field + */ +export interface ReceiptItem { + /** + * Name of the receipt item + */ + name?: string; + /** + * Price of the receipt item + */ + price?: number; + /** + * Quantity of the receipt item + */ + quantity?: number; + /** + * Total price of the receipt item + */ + totalPrice?: number; +} + +/** + * Represents a list of recognized receipt items in a receipt. + */ +export interface ReceiptItemArrayField { + type: "array"; + value: ReceiptItemField[]; +} + +/** + * Represents recognized receipt fields in a receipt + */ +export interface RawUSReceipt { + /** + * Receipt type field + */ + ReceiptType: StringFieldValue; + /** + * Merchant name field + */ + MerchantName: StringFieldValue; + /** + * Merchant phone number field + */ + MerchantPhoneNumber: PhoneNumberFieldValue; + /** + * Merchant address field + */ + MerchantAddress: StringFieldValue; + /** + * Receipt item list field + */ + Items: ReceiptItemArrayField; + /** + * Subtotal field + */ + Subtotal: NumberFieldValue; + /** + * Tax field + */ + Tax: NumberFieldValue; + /** + * Tip field + */ + Tip: NumberFieldValue; + /** + * Total field + */ + Total: NumberFieldValue; + /** + * Transaction date field + */ + TransactionDate: DateFieldValue; + /** + * Transaction time field + */ + TransactionTime: TimeFieldValue; +} + +/** + * Represents text values in a receipt + */ +export interface Receipt { + /** + * Receipt type, e.g., "Itemized" + */ + receiptType: string; + /** + * Merchant name + */ + merchantName?: string; + /** + * Merchant phone number + */ + merchantPhoneNumber?: string; + /** + * Merchant address + */ + merchantAddress?: string; + /** + * items in the receipt + */ + items: ReceiptItem[]; + /** + * Subtotal + */ + subtotal?: number; + /** + * Tax + */ + tax?: number; + /** + * Tip + */ + tip?: number; + /** + * Total + */ + total?: number; + /** + * Transaction date + */ + transactionDate?: Date; + /** + * Transaction time + */ + transactionTime?: string; +} + +/** + * Represents an recognized receipt + */ +export interface RawReceiptResult { + /** + * Document type. + */ + docType: "prebuilt:receipt"; + /** + * First and last page number where the document is found. + */ + pageRange: FormPageRange; + /** + * Dictionary of named field values. + */ + fields: { [propertyName: string]: FieldValue }; +} + +/** + * Recognized receipt and values in it + */ +export type RecognizedReceipt = RawReceiptResult & Receipt; + +/** + * Raw texts recognized from a page in the input document. + */ +export interface FormPage { + /** + * The 1-based page number in the input document. + */ + pageNumber: number; + /** + * The general orientation of the text in clockwise direction, measured in degrees between (-180, + * 180]. + */ + angle: number; + /** + * The width of the image/PDF in pixels/inches, respectively. + */ + width: number; + /** + * The height of the image/PDF in pixels/inches, respectively. + */ + height: number; + /** + * The unit used by the width, height and boundingBox properties. For images, the unit is + * "pixel". For PDF, the unit is "inch". Possible values include: 'pixel', 'inch' + */ + unit: LengthUnit; + /** + * The detected language on the page overall. Possible values include: 'en', 'es' + */ + // language?: Language; + /** + * When includeTextDetails is set to true, a list of recognized text lines. The maximum number of + * lines returned is 300 per page. The lines are sorted top to bottom, left to right, although in + * certain cases proximity is treated with higher priority. As the sorting order depends on the + * detected text, it may change across images and OCR version updates. Thus, business logic + * should be built upon the actual line location instead of order. + */ + lines?: FormLine[]; +} + +/** + * Analyze Receipt result. + */ +export interface RecognizeReceiptResult { + /** + * Version of schema used for this result. + */ + version: string; + /** + * List of raw text line information on recognized pages + */ + rawExtractedPages: FormPage[]; + /** + * List of receipts recognized from input document + */ + extractedReceipts?: RecognizedReceipt[]; +} + +/** + * Results of an recognize receipt operation + */ +export type RecognizeReceiptOperationResult = { + /** + * Operation status. + */ + status: OperationStatus; // 'notStarted' | 'running' | 'succeeded' | 'failed'; + /** + * Date and time (UTC) when the analyze operation was submitted. + */ + createdOn: Date; + /** + * Date and time (UTC) when the status was last updated. + */ + lastUpdatedOn: Date; +} & Partial; + +/** + * Contains response data for an recognize receipt operation. + */ +export type RecognizeReceiptResultResponse = RecognizeReceiptOperationResult & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: AnalyzeOperationResultModel; + }; +}; + +/** + * Recognized information about the layout of the analyzed document + */ +export interface RecognizedContent { + /** + * Version of schema used for this result. + */ + version: string; + /** + * Raw texts extracted from a page in the input + */ + rawExtractedPages: FormPage[]; + /** + * Form layout recognized from a page in the input + */ + extractedLayoutPages?: RecognizedContentPage[]; +} + +/** + * Represents an recognized content page + */ +export interface RecognizedContentPage { + /** + * List of name/value pairs recognized from the page + */ + fields?: FormField[]; + /** + * Page number + */ + pageNumber: number; + /** + * List of data tables recognized form the page + */ + tables?: FormTable[]; +} + +/** + * Represents the result from an Recognize Content operation + */ +export type RecognizeContentOperationResult = { + /** + * Operation status. + */ + status: OperationStatus; // 'notStarted' | 'running' | 'succeeded' | 'failed'; + /** + * Date and time (UTC) when the analyze operation was submitted. + */ + createdOn: Date; + /** + * Date and time (UTC) when the status was last updated. + */ + lastUpdatedOn: Date; +} & Partial; + +/** + * Contains response data for the Recognize Content operation. + */ +export type RecognizeContentResultResponse = RecognizeContentOperationResult & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: AnalyzeOperationResultModel; + }; +}; + +/** + * Represents an recognized form using unsupervised model + */ +export interface FormResult { + /** + * Version of schema used for this result. + */ + version: string; + /** + * Text recognized from the input. + */ + rawExtractedPages: FormPage[]; + /** + * Page-level information recognized from the input. + */ + extractedPages?: RecognizedPage[]; + /** + * List of errors reported during the analyze operation. + */ + errors?: ErrorInformation[]; +} + +/** + * Represents the result from an recognize form operation using unsupervised model + */ +export type RecognizeFormOperationResult = Partial & { + /** + * Operation status. + */ + status: OperationStatus; + /** + * Date and time (UTC) when the analyze operation was submitted. + */ + createdOn: Date; + /** + * Date and time (UTC) when the status was last updated. + */ + lastUpdatedOn: Date; +}; + +/** + * Represents an recognized form using a model from supervised training with labels + */ +export interface LabeledFormResult { + /** + * Version of schema used for this result. + */ + version: string; + /** + * Text recognized from the input. Example includes lines consist of words, and other + * elements like checkbox, etc. + */ + rawExtractedPages: FormPage[]; + /** + * Page-level information recognized from the input, including fields of name-value pairs + * and data tables. + */ + extractedPages?: RecognizedPage[]; + /** + * Document-level information recognized from the input using machine learning. They include + * recognized fields that have meaning beyond text, for example, addresses, phone numbers, dates, etc. + */ + extractedForms?: RecognizedForm[]; + /** + * List of errors reported during the analyze operation. + */ + errors?: ErrorInformation[]; +} + +/** + * Represents the result from an recognize form operation using a supervised model + */ +export type LabeledFormOperationResult = Partial & { + /** + * Operation status. + */ + status: OperationStatus; + /** + * Date and time (UTC) when the analyze operation was submitted. + */ + createdOn: Date; + /** + * Date and time (UTC) when the status was last updated. + */ + lastUpdatedOn: Date; +}; + +/** + * Contains the response data for recognize form (unsupervised) operation + */ +export type RecognizeFormResultResponse = RecognizeFormOperationResult & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: AnalyzeOperationResultModel; + }; +}; + +/** + * Contains the response data for recognize form (supervised) operation + */ +export type LabeledFormResultResponse = LabeledFormOperationResult & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: AnalyzeOperationResultModel; + }; +}; + +/** + * Contains the unsupervised training results + */ +export interface FormTrainResult { + /** + * List of document used to train the model and any errors reported for each document. + */ + trainingDocuments: TrainingDocumentInfo[]; + /** + * Errors returned during training operation. + */ + errors?: ErrorInformation[]; +} + +/** + * Represents a model from unsupervised training + */ +export interface FormModel { + /** + * Information about the model + */ + modelInfo: ModelInfo; + /** + * Keys recognized from unsupervised training + */ + keys: KeysResult; + /** + * Results of the unsupervised training + */ + trainResult?: FormTrainResult; +} + +/** + * Contains the response data for retrieving a model from unsupervised training + */ +export type FormModelResponse = FormModel & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: Model; + }; +}; + +/** + * Represents result of supervised training with labels + */ +export interface LabeledFormTrainResult { + /** + * List of the documents used to train the model and any errors reported in each document. + */ + trainingDocuments: TrainingDocumentInfo[]; + /** + * List of fields used to train the model and the train operation error reported by each. + */ + fields: FormFieldsReport[]; + /** + * Average accuracy. + */ + averageModelAccuracy: number; + /** + * Errors returned during the training operation. + */ + errors?: ErrorInformation[]; +} + +/** + * Represents the trained model from supervised training with labels + */ +export interface LabeledFormModel { + /** + * Information about the model in supervised training + */ + modelInfo: ModelInfo; + /** + * Results of the supervised training with label files + */ + trainResult?: LabeledFormTrainResult; +} + +/** + * Contains the response data for retrieving a model from supervised training with labels + */ +export type LabeledFormModelResponse = LabeledFormModel & { + /** + * The underlying HTTP response. + */ + _response: coreHttp.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: Model; + }; +}; + +/** + * Types of input data allowed to analyze operations + */ +export type FormRecognizerRequestBody = + | Blob + | ArrayBuffer + | ArrayBufferView + | NodeJS.ReadableStream; diff --git a/sdk/formrecognizer/ai-form-recognizer/src/shims.d.ts b/sdk/formrecognizer/ai-form-recognizer/src/shims.d.ts new file mode 100644 index 000000000000..7962a0d7e62f --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/shims.d.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/* eslint @typescript-eslint/no-empty-interface: 0 */ + +// d.ts shims provide types for things we use internally but are not part +// of this package's surface area. + +interface RequestInit {} + +interface RequestInfo {} + +interface Response {} + +interface Headers {} + +interface FileReader { + onloadend: ((this: FileReader, ev: any) => any) | null; + readonly result: string | ArrayBuffer | null; + readAsArrayBuffer(blob: Blob): void; +} + +declare var FileReader: { + prototype: FileReader; + new (): FileReader; + readonly DONE: number; + readonly EMPTY: number; + readonly LOADING: number; +}; diff --git a/sdk/formrecognizer/ai-form-recognizer/src/tracing.ts b/sdk/formrecognizer/ai-form-recognizer/src/tracing.ts new file mode 100644 index 000000000000..f52c04a1878f --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/tracing.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { getTracer } from "@azure/core-tracing"; +import { Span, SpanOptions, SpanKind } from "@opentelemetry/types"; +import { OperationOptions } from "@azure/core-http"; + +type OperationTracingOptions = OperationOptions["tracingOptions"]; + +/** + * Creates a span using the global tracer. + * @ignore + * @param name The name of the operation being performed. + * @param tracingOptions The options for the underlying http request. + */ +export function createSpan( + operationName: string, + operationOptions: T +): { span: Span; updatedOptions: T } { + const tracer = getTracer(); + const tracingOptions = operationOptions.tracingOptions || {}; + const spanOptions: SpanOptions = { + ...tracingOptions.spanOptions, + kind: SpanKind.INTERNAL + }; + + const span = tracer.startSpan( + `Azure.CognitiveServices.FormRecognizer.${operationName}`, + spanOptions + ); + + span.setAttribute("az.namespace", "Microsoft.CognitiveServices"); + + let newSpanOptions = tracingOptions.spanOptions || {}; + if (span.isRecording()) { + newSpanOptions = { + ...tracingOptions.spanOptions, + parent: span, + attributes: { + ...spanOptions.attributes, + "az.namespace": "Microsoft.CognitiveServices" + } + }; + } + + const newTracingOptions: OperationTracingOptions = { + ...tracingOptions, + spanOptions: newSpanOptions + }; + + const newOperationOptions: T = { + ...operationOptions, + tracingOptions: newTracingOptions + }; + + return { + span, + updatedOptions: newOperationOptions + }; +} diff --git a/sdk/formrecognizer/ai-form-recognizer/src/transforms.ts b/sdk/formrecognizer/ai-form-recognizer/src/transforms.ts new file mode 100644 index 000000000000..d1bbdae39c76 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/transforms.ts @@ -0,0 +1,424 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + AnalyzeResult as AnalyzeResultModel, + DataTable as DataTableModel, + DocumentResult as DocumentResultModel, + FieldValue as FieldValueModel, + KeyValueElement as KeyValueElementModel, + KeyValuePair as KeyValuePairModel, + PageResult as PageResultModel, + ReadResult as ReadResultModel, + TextLine as TextLineModel, + FormRecognizerClientGetAnalyzeFormResultResponse as GetAnalyzeFormResultResponse, + FormRecognizerClientGetAnalyzeLayoutResultResponse as GetAnalyzeLayoutResultResponse, + FormRecognizerClientGetAnalyzeReceiptResultResponse as GetAnalyzeReceiptResultResponse +} from "./generated/models"; + +import { + FormPage, + FormLine, + FormElement, + FormTableRow, + FormTable, + RecognizedForm, + FormText, + FormField, + RecognizedPage, + LabeledFormResultResponse, + RecognizeFormResultResponse, + RecognizeContentResultResponse, + RecognizedContent, + RecognizeReceiptResultResponse, + FieldValue, + RawReceiptResult, + RecognizedReceipt, + RawUSReceipt, + ReceiptItemField, + StringFieldValue, + DateFieldValue, + TimeFieldValue, + PhoneNumberFieldValue, + NumberFieldValue, + IntegerFieldValue, + ObjectFieldValue, + ArrayFieldValue, + Point2D +} from "./models"; + +export function toBoundingBox(original: number[]): Point2D[] { + return [ + {x: original[0], y: original[1] }, + {x: original[2], y: original[3] }, + {x: original[4], y: original[5] }, + {x: original[6], y: original[7] } + ] +} + +export function toTextLine(original: TextLineModel, pageNumber: number): FormLine { + const line: FormLine = { + kind: "line", + pageNumber: pageNumber, + text: original.text, + boundingBox: toBoundingBox(original.boundingBox), + words: original.words.map((w) => { + return { + kind: "word", + text: w.text, + boundingBox: toBoundingBox(w.boundingBox), + confidence: w.confidence, + pageNumber: pageNumber + }; + }) + }; + line.words = line.words.map((w) => { + return { ...w, containingLine: line }; + }); + + return line; +} + +export function toFormPage(original: ReadResultModel): FormPage { + return { + pageNumber: original.pageNumber, + angle: original.angle, + width: original.width, + height: original.height, + unit: original.unit, + lines: original.lines?.map((l) => toTextLine(l, original.pageNumber)) + }; +} + +// Note: might need to support other element types in future, e.g., checkbox +const textPattern = /#\/readResults\/(\d+)\/lines\/(\d+)(?:\/words\/(\d+))?/; + +export function toFormElement( + element: string, + readResults: FormPage[] +): FormElement { + const result = textPattern.exec(element); + if (!result || !result[0] || !result[1] || !result[2]) { + throw new Error(`Unexpected element reference encountered: ${element}`); + } + + const readIndex = Number.parseInt(result[1]); + const lineIndex = Number.parseInt(result[2]); + if (result[3]) { + const wordIndex = Number.parseInt(result[3]); + return readResults[readIndex].lines![lineIndex].words[wordIndex]; + } else { + return readResults[readIndex].lines![lineIndex]; + } +} + +export function toFormText( + original: KeyValueElementModel, + readResults?: FormPage[] +): FormText { + return { + text: original.text, + boundingBox: original.boundingBox === undefined ? undefined : toBoundingBox(original.boundingBox), + elements: original.elements?.map((element) => toFormElement(element, readResults!)) + }; +} + +export function toFormField( + original: KeyValuePairModel, + readResults?: FormPage[] +): FormField { + return { + label: original.label, + confidence: original.confidence, + fieldLabel: toFormText(original.key, readResults), + valueText: toFormText(original.value, readResults) + }; +} + +export function toFormTable(original: DataTableModel, readResults?: FormPage[]): FormTable { + let rows: FormTableRow[] = []; + for (let i = 0; i < original.rows; i++) { + rows.push({ cells: [] }); + } + for (const cell of original.cells) { + rows[cell.rowIndex].cells.push({ + boundingBox: toBoundingBox(cell.boundingBox), + columnIndex: cell.columnIndex, + columnSpan: cell.columnSpan || 1, + confidence: cell.confidence, + elements: cell.elements?.map((element) => toFormElement(element, readResults!)), + isFooter: cell.isFooter || false, + isHeader: cell.isHeader || false, + rowIndex: cell.rowIndex, + rowSpan: cell.rowSpan || 1, + text: cell.text + }); + } + return { + rowCount: original.rows, + columnCount: original.columns, + rows: rows + }; +} + +export function toRecognizedPage( + original: PageResultModel, + readResults?: FormPage[] +): RecognizedPage { + return { + pageNumber: original.pageNumber, + formTypeId: original.clusterId, + fields: original.keyValuePairs?.map((pair) => toFormField(pair, readResults)), + tables: original.tables?.map((table) => toFormTable(table, readResults)) + }; +} + +export function transformResults( + readResults?: ReadResultModel[], + pageResults?: PageResultModel[] +): { rawExtractedPages: FormPage[]; extractedPages: RecognizedPage[] } { + const transformedReadResults = readResults?.map(toFormPage); + return { + rawExtractedPages: transformedReadResults || [], + extractedPages: pageResults?.map((page) => toRecognizedPage(page, transformedReadResults)) || [] + }; +} + +export function toRecognizeFormResultResponse( + original: GetAnalyzeFormResultResponse +): RecognizeFormResultResponse { + const { rawExtractedPages, extractedPages } = transformResults( + original.analyzeResult?.readResults, + original.analyzeResult?.pageResults + ); + const common = { + status: original.status, + createdOn: original.createdOn, + lastUpdatedOn: original.createdOn, + _response: original._response + }; + + if (original.status !== "succeeded") { + return common; + } + + const additional = original.analyzeResult + ? { + version: original.analyzeResult.version, + rawExtractedPages, + extractedPages, + errors: original.analyzeResult.errors + } + : undefined; + return { + ...common, + ...additional + }; +} + +export function toFieldValue( + original: FieldValueModel, + readResults: FormPage[] +): FieldValue { + const result = + original.type === "object" || original.type === "array" + ? {} + : { + text: original.text, + boundingBox: original.boundingBox === undefined ? undefined : toBoundingBox(original.boundingBox), + confidence: original.confidence, + pageNumber: original.pageNumber, + elements: original.elements?.map((element) => toFormElement(element, readResults)) + }; + switch (original.type) { + case "string": + (result as StringFieldValue).type = "string"; + (result as StringFieldValue).value = original.valueString; + break; + case "date": + (result as DateFieldValue).type = "date"; + (result as DateFieldValue).value = original.valueDate; + break; + case "time": + (result as TimeFieldValue).type = "time"; + (result as TimeFieldValue).value = original.valueTime; + break; + case "phoneNumber": + (result as PhoneNumberFieldValue).type = "phoneNumber"; + (result as PhoneNumberFieldValue).value = original.valuePhoneNumber; + break; + case "number": + (result as NumberFieldValue).type = "number"; + (result as NumberFieldValue).value = original.valueNumber; + break; + case "integer": + (result as IntegerFieldValue).type = "integer"; + (result as IntegerFieldValue).value = original.valueInteger; + break; + case "array": + (result as ArrayFieldValue).type = "array"; + (result as ArrayFieldValue).value = original.valueArray?.map((a) => + toFieldValue(a, readResults) + ); + break; + case "object": + (result as ObjectFieldValue).type = "object"; + (result as ObjectFieldValue).value = original.valueObject + ? toFields(original.valueObject, readResults) + : undefined; + break; + default: + throw new Error(`Unknown field value type from ${original}`); + } + return (result as unknown) as FieldValue; +} + +export function toFields( + original: { [propertyName: string]: FieldValueModel }, + readResults: FormPage[] +): { [propertyName: string]: FieldValue } { + const result: { [propertyName: string]: FieldValue } = {}; + for (const key in original) { + if (original.hasOwnProperty(key)) { + result[key] = toFieldValue(original[key], readResults); + } + } + + return result; +} + +function toRecognizedForm( + original: DocumentResultModel, + readResults: FormPage[] +): RecognizedForm { + return { + docType: original.docType, + pageRange: { firstPageNumber: original.pageRange[0], lastPageNumber: original.pageRange[1] }, + fields: toFields(original.fields, readResults) + }; +} + +export function toLabeledFormResultResponse( + original: GetAnalyzeFormResultResponse +): LabeledFormResultResponse { + const common = { + status: original.status, + createdOn: original.createdOn, + lastUpdatedOn: original.createdOn, + _response: original._response + }; + if (original.status !== "succeeded") { + return common; + } + const { rawExtractedPages, extractedPages } = transformResults( + original.analyzeResult?.readResults, + original.analyzeResult?.pageResults + ); + const additional = original.analyzeResult + ? { + version: original.analyzeResult.version, + extractedForms: original.analyzeResult.documentResults?.map((d) => + toRecognizedForm(d, rawExtractedPages) + ), + rawExtractedPages, + extractedPages, + errors: original.analyzeResult.errors + } + : undefined; + return { + ...common, + ...additional + }; +} + +export function toAnalyzeLayoutResultResponse( + original: GetAnalyzeLayoutResultResponse +): RecognizeContentResultResponse { + function toAnalyzeLayoutResult(model?: AnalyzeResultModel): RecognizedContent | undefined { + if (!model) { + return undefined; + } + const { rawExtractedPages, extractedPages } = transformResults( + model.readResults, + model.pageResults + ); + return { + version: model.version, + rawExtractedPages, + extractedLayoutPages: extractedPages + }; + } + + const common = { + status: original.status, + createdOn: original.createdOn, + lastUpdatedOn: original.lastUpdatedOn, + _response: original._response + }; + if (original.status === "succeeded") { + return { + ...common, + ...toAnalyzeLayoutResult(original.analyzeResult) + }; + } else { + return common; + } +} + +function toReceiptResult( + result: DocumentResultModel, + readResults: FormPage[] +): RecognizedReceipt { + if (result.docType !== "prebuilt:receipt") { + throw new RangeError("The document type is not 'prebuilt:receipt'"); + } + + const transformedFields = toFields(result.fields, readResults); + const rawReceiptFields = (transformedFields as unknown) as RawUSReceipt; + return { + docType: ((result as unknown) as RawReceiptResult).docType, + pageRange: { firstPageNumber: result.pageRange[0], lastPageNumber: result.pageRange[1] }, + receiptType: rawReceiptFields.ReceiptType.value!, + merchantName: rawReceiptFields.MerchantName?.value, + merchantPhoneNumber: rawReceiptFields.MerchantPhoneNumber?.value, + merchantAddress: rawReceiptFields.MerchantAddress?.value, + items: rawReceiptFields.Items.value?.map((i) => { + return { + name: (i as ReceiptItemField).value.Name?.value, + quantity: (i as ReceiptItemField).value.Quantity?.value, + totalPrice: (i as ReceiptItemField).value.TotalPrice?.value + }; + }), + subtotal: rawReceiptFields.Subtotal?.value, + tax: rawReceiptFields.Tax?.value, + tip: rawReceiptFields.Tip?.value, + total: rawReceiptFields.Total?.value, + transactionDate: rawReceiptFields.TransactionDate?.value, + transactionTime: rawReceiptFields.TransactionTime?.value, + fields: transformedFields + }; +} + +export function toReceiptResultResponse( + result: GetAnalyzeReceiptResultResponse +): RecognizeReceiptResultResponse { + const common = { + status: result.status, + createdOn: result.createdOn, + lastUpdatedOn: result.lastUpdatedOn, + _response: result._response + }; + if (result.status !== "succeeded") { + return common; + } + + const readResults = result.analyzeResult!.readResults.map(toFormPage); + return { + ...common, + version: result.analyzeResult!.version, + rawExtractedPages: readResults, + extractedReceipts: result.analyzeResult!.documentResults!.map((d) => + toReceiptResult(d, readResults) + ) + }; +} diff --git a/sdk/formrecognizer/ai-form-recognizer/src/utils/utils.browser.ts b/sdk/formrecognizer/ai-form-recognizer/src/utils/utils.browser.ts new file mode 100644 index 000000000000..25d698d52d15 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/utils/utils.browser.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export function streamToBuffer() {} diff --git a/sdk/formrecognizer/ai-form-recognizer/src/utils/utils.node.ts b/sdk/formrecognizer/ai-form-recognizer/src/utils/utils.node.ts new file mode 100644 index 000000000000..038887170773 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/src/utils/utils.node.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +const SIZE_ONE_MEGA = 1024 * 1024 + +/** + * Reads a readable stream into buffer entirely. NodeJS only. + * The maximum allowed size is specified in {@link MAX_INPUT_DOCUMENT_SIZE}. + * + * @export + * @param {NodeJS.ReadableStream} stream A Node.js Readable stream + * @returns {Promise} The resultant buffer. + * @throws {Error} If buffer size is not big enough. + */ +export async function streamToBuffer(stream: NodeJS.ReadableStream, maxSize: number): Promise { + let pos = 0; // Position in stream + let size = SIZE_ONE_MEGA; + let buffer = Buffer.alloc(size); + + return new Promise((resolve, reject) => { + stream.on("readable", () => { + let chunk = stream.read(); + if (!chunk) { + return; + } + const nextPos = pos + chunk.length; + if (size < nextPos && nextPos <= maxSize) { + // Keep doubling buffer until it is large enough or over max size + const oldSize = size; + while (size < nextPos && size * 2 < maxSize) { + size *= 2; + } + + if (nextPos < size && size < maxSize) { + const newBuffer = Buffer.alloc(size - oldSize); + buffer = Buffer.concat([buffer, newBuffer]); + } else { + const newBuffer = Buffer.alloc(maxSize - oldSize); + size = maxSize; + buffer = Buffer.concat([buffer, newBuffer]); + } + } else if (nextPos > maxSize) { + reject(new Error(`Input stream exceeds maximum allowed size: ${maxSize}`)); + return; + } + + buffer.fill(chunk, pos, nextPos); + pos = nextPos; + }); + + stream.on("end", () => { + resolve(buffer.slice(0, pos)); + }); + + stream.on("error", reject); + }); +} diff --git a/sdk/formrecognizer/ai-form-recognizer/swagger/README.md b/sdk/formrecognizer/ai-form-recognizer/swagger/README.md new file mode 100644 index 000000000000..8e779c3a1873 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/swagger/README.md @@ -0,0 +1,55 @@ +# Azure Form Recognizer Protocol Layer + +> see https://aka.ms/autorest + +## Configuration + +```yaml +v3: true +package-name: "@azure/ai-form-recognizer" +title: FormRecognizerClient +description: FormRecognizer Client +generate-metadata: false +license-header: MICROSOFT_MIT_NO_VERSION +output-folder: ../ +source-code-folder-path: ./src/generated +input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/e500fcb5e7483595fb1f1a2a16586fd15d0882eb/specification/cognitiveservices/data-plane/FormRecognizer/preview/v2.0/FormRecognizer.json +add-credentials: true +use-extension: + "@autorest/typescript": "6.0.0-dev.20200320.1" +``` + +## Customizations for Track 2 Generator + +See the [AutoRest samples](https://github.com/Azure/autorest/tree/master/Samples/3b-custom-transformations) +for more about how we're customizing things. + +### `page` property renamed to `pageNumber` + +```yaml +directive: + - from: swagger-document + where: $.definitions..properties.page + transform: > + $["x-ms-client-name"] = "pageNumber"; +``` + +### `createdDateTime` => `createdOn` + +```yaml +directive: + - from: swagger-document + where: $.definitions..properties.createdDateTime + transform: > + $["x-ms-client-name"] = "createdOn"; +``` + +### `lastUpdatedDateTime` => `lastUpdatedOn` + +```yaml +directive: + - from: swagger-document + where: $.definitions..properties.lastUpdatedDateTime + transform: > + $["x-ms-client-name"] = "lastUpdatedOn"; +``` diff --git a/sdk/formrecognizer/ai-form-recognizer/test/common.spec.ts b/sdk/formrecognizer/ai-form-recognizer/test/common.spec.ts new file mode 100644 index 000000000000..eb375e450239 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/test/common.spec.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { getContentType, toRequestBody } from "../src/common"; + +describe("getContentType()", () => { + it("identifies ArrayBuffer of application/pdf", async () => { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer); + view.set([0x25, 0x50, 0x44, 0x46]); + + const result = await getContentType(buffer); + assert.equal(result, "application/pdf"); + }); + + it("identifies ArrayBufferView of application/pdf", async () => { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer); + view.set([0x25, 0x50, 0x44, 0x46]); + + const result = await getContentType(view); + assert.equal(result, "application/pdf"); + }); + + it("identifies ArrayBuffer of image/jpeg", async () => { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer); + view.set([0xff, 0xd8, 0x0, 0x0]); + + const result = await getContentType(buffer); + assert.equal(result, "image/jpeg"); + }); + + it("identifies ArrayBufferView of image/jpeg", async () => { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer); + view.set([0xff, 0xd8, 0x0, 0x0]); + + const result = await getContentType(view); + assert.equal(result, "image/jpeg"); + }); + + it("identifies ArrayBuffer of image/png", async () => { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer); + view.set([0x89, 0x50, 0x4e, 0x47]); + + const result = await getContentType(buffer); + assert.equal(result, "image/png"); + }); + + it("identifies ArrayBufferView of image/png", async () => { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer); + view.set([0x89, 0x50, 0x4e, 0x47]); + + const result = await getContentType(view); + assert.equal(result, "image/png"); + }); + + it("identifies ArrayBuffer of image/tiff little-endian", async () => { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer); + view.set([0x49, 0x49, 0x2a, 0x0]); + + const result = await getContentType(buffer); + assert.equal(result, "image/tiff"); + }); + + it("identifies ArrayBufferView of image/tiff little-endian", async () => { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer); + view.set([0x49, 0x49, 0x2a, 0x0]); + + const result = await getContentType(view); + assert.equal(result, "image/tiff"); + }); + + it("identifies ArrayBuffer of image/tiff big-endian", async () => { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer); + view.set([0x4d, 0x4d, 0x0, 0x2a]); + + const result = await getContentType(buffer); + assert.equal(result, "image/tiff"); + }); + + it("identifies ArrayBufferView of image/tiff big-endian", async () => { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer); + view.set([0x4d, 0x4d, 0x0, 0x2a]); + + const result = await getContentType(view); + assert.equal(result, "image/tiff"); + }); + + it("throws error with input of byte length less than 4", async () => { + const buffer = new ArrayBuffer(2); + const view = new Uint8Array(buffer); + view.set([0xff, 0xd8]); + + try { + await getContentType(buffer); + throw new Error("An error should have already been thrown"); + } catch (err) { + assert.equal((err as Error).message, "Invalid input. Expect more than 4 bytes of data"); + } + }); + + it("throws error with input of unsupported format", async () => { + const buffer = new ArrayBuffer(10); + const view = new Uint8Array(buffer); + view.set([0xaa, 0xaa, 0xaa, 0xaa, 0xaa]); + + try { + await getContentType(buffer); + throw new Error("An error should have already been thrown"); + } catch (err) { + assert.equal((err as Error).message, "content type could not be detected"); + } + }); +}).timeout(60000); + +describe("toRequestBody()", () => { + it("converts string url to SourcePath", async () => { + const result = await toRequestBody("http://url"); + + assert.deepStrictEqual(result, { source: "http://url" }); + }); +}).timeout(60000); diff --git a/sdk/formrecognizer/ai-form-recognizer/test/node/utils.spec.ts b/sdk/formrecognizer/ai-form-recognizer/test/node/utils.spec.ts new file mode 100644 index 000000000000..65055ad8eca7 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/test/node/utils.spec.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { toRequestBody } from "../../src/common"; +import { PassThrough } from "stream"; +import { streamToBuffer } from '../../src/utils/utils.node'; + +describe("toRequestBody() NodeJS only", () => { + it("cache readable stream to Buffer", async () => { + const buf = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]); + const bufferStream = new PassThrough(); + bufferStream.end(buf); + + const result = await toRequestBody(bufferStream) as Buffer; + + assert.equal(Buffer.compare(result, buf), 0, "Expected the result buffer to have the same binary data"); + }); +}).timeout(60000); + +describe("utils NodeJS only", () => { + it("streamToBuffer() should work when input is smaller than 1 MB", async () => { + const dataLength = 1 * 1024 * 1024 - 5; + const data = Buffer.alloc(dataLength); + data.fill('a'); + + const bufferStream = new PassThrough(); + bufferStream.end(data); + + const result = await streamToBuffer(bufferStream, 3 * 1024 * 1024) as Buffer; + + assert.equal(result.byteLength, dataLength); + }); + + it("streamToBuffer() should work when input is larger than 1 MB", async () => { + const dataLength = 2 * 1024 * 1024 - 5; + const data = Buffer.alloc(dataLength); + data.fill('a'); + + const bufferStream = new PassThrough(); + bufferStream.end(data); + + const result = await streamToBuffer(bufferStream, 3 * 1024 * 1024) as Buffer; + + assert.equal(result.byteLength, dataLength); + }); + + it("streamToBuffer() should work when internal buffer expands multiple times", async () => { + const dataLength = 7 * 1024 * 1024; + const data = Buffer.alloc(dataLength); + data.fill('a'); + + const bufferStream = new PassThrough(); + bufferStream.end(data); + + const result = await streamToBuffer(bufferStream, 8 * 1024 * 1024) as Buffer; + + assert.equal(result.byteLength, dataLength); + }); + + it("streamToBuffer() should work when input is multiple of 1 MB", async () => { + const dataLength = 2 * 1024 * 1024; + const data = Buffer.alloc(dataLength); + data.fill('a'); + + const bufferStream = new PassThrough(); + bufferStream.end(data); + + const result = await streamToBuffer(bufferStream, 3 * 1024 * 1024) as Buffer; + + assert.equal(result.byteLength, dataLength); + }); + + + it("streamToBuffer() should work when input is slightly less than max allowed", async () => { + const dataLength = 3 * 1024 * 1024 - 1; + const data = Buffer.alloc(dataLength); + data.fill('a'); + + const bufferStream = new PassThrough(); + bufferStream.end(data); + + const result = await streamToBuffer(bufferStream, 3 * 1024 * 1024) as Buffer; + + assert.equal(result.byteLength, dataLength); + }); + + it("streamToBuffer() should work when input has the same size as max size", async () => { + const dataLength = 3 * 1024 * 1024; + const data = Buffer.alloc(dataLength); + data.fill('a'); + + const bufferStream = new PassThrough(); + bufferStream.end(data); + + const result = await streamToBuffer(bufferStream, 3 * 1024 * 1024) as Buffer; + + assert.equal(result.byteLength, dataLength); + }); + + it("streamToBuffer() should throw if input size is larger than max size", async () => { + const dataLength = 3 * 1024 * 1024; + const data = Buffer.alloc(dataLength); + data.fill('a'); + + const bufferStream = new PassThrough({highWaterMark: 20 * 1024}); + bufferStream.end(data); + + try { + await streamToBuffer(bufferStream, 2 * 1024 * 1024) as Buffer; + throw new Error("An error should have been thrown") + } catch (err) { + assert.equal((err as Error).message, `Input stream exceeds maximum allowed size: ${2 * 1024 * 1024}`); + } + }); +}).timeout(60000); diff --git a/sdk/formrecognizer/ai-form-recognizer/test/tracing.spec.ts b/sdk/formrecognizer/ai-form-recognizer/test/tracing.spec.ts new file mode 100644 index 000000000000..84994dca0a2e --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/test/tracing.spec.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import * as sinon from "sinon"; +import { createSpan } from "../src/tracing"; +import { setTracer, TestTracer, TestSpan } from "@azure/core-tracing"; +import { SpanKind } from "@opentelemetry/types"; +import { OperationOptions } from "@azure/core-auth"; + +describe("tracing.createSpan", () => { + it("returns a created span with the right metadata", () => { + const tracer = new TestTracer(); + const testSpan = new TestSpan( + tracer, + "testing", + { traceId: "", spanId: "" }, + SpanKind.INTERNAL + ); + const setAttributeSpy = sinon.spy(testSpan, "setAttribute"); + const startSpanStub = sinon.stub(tracer, "startSpan"); + startSpanStub.returns(testSpan); + setTracer(tracer); + const { span } = createSpan("testOperation", {}); + assert.strictEqual(span, testSpan, "Should return mocked span"); + assert.isTrue(startSpanStub.calledOnce); + const [name, options] = startSpanStub.firstCall.args; + assert.strictEqual(name, "Azure.CognitiveServices.FormRecognizer.testOperation"); + assert.deepEqual(options, { kind: SpanKind.INTERNAL }); + assert.isTrue( + setAttributeSpy.calledOnceWithExactly("az.namespace", "Microsoft.CognitiveServices") + ); + }); + + it("returns updated SpanOptions", () => { + const options: OperationOptions = {}; + const { span, updatedOptions } = createSpan("testOperation", options); + assert.isEmpty(options, "original options should not be modified"); + assert.notStrictEqual(updatedOptions, options, "should return new object"); + const expected: OperationOptions = { + tracingOptions: { + spanOptions: { + parent: span, + attributes: { + "az.namespace": "Microsoft.CognitiveServices" + } + } + } + }; + assert.deepEqual(updatedOptions, expected); + }); + + it("preserves existing attributes", () => { + const options: OperationOptions = { + tracingOptions: { + spanOptions: { + attributes: { + foo: "bar" + } + } + } + }; + const { span, updatedOptions } = createSpan("testOperation", options); + assert.notStrictEqual(updatedOptions, options, "should return new object"); + const expected: OperationOptions = { + tracingOptions: { + spanOptions: { + parent: span, + attributes: { + "az.namespace": "Microsoft.CognitiveServices", + foo: "bar" + } + } + } + }; + assert.deepEqual(updatedOptions, expected); + }); + + afterEach(() => { + sinon.restore(); + }); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/test/transforms.spec.ts b/sdk/formrecognizer/ai-form-recognizer/test/transforms.spec.ts new file mode 100644 index 000000000000..1fd7c2490db1 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/test/transforms.spec.ts @@ -0,0 +1,399 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { + toTextLine, + toFormPage, + toFormElement, + toFormText, + toFormField, + toFieldValue, + toFormTable +} from "../src/transforms"; +import { + ReadResult as ReadResultModel, + FieldValue as FieldValueModel, + DataTable as DataTableModel +} from "../src/generated/models"; +import { + StringFieldValue, + DateFieldValue, + TimeFieldValue, + PhoneNumberFieldValue, + NumberFieldValue, + IntegerFieldValue, + ArrayFieldValue, + ObjectFieldValue, + Point2D +} from "../src/models"; + +describe("Transforms", () => { + function verifyBoundingBox(transformed: Point2D[], original: number[]): void { + assert.equal(transformed[0].x, original[0], "Expect transform[0].x to equal original[0]"); + assert.equal(transformed[0].y, original[1], "Expect transform[0].y to equal original[1]"); + assert.equal(transformed[1].x, original[2], "Expect transform[1].x to equal original[2]"); + assert.equal(transformed[1].y, original[3], "Expect transform[1].y to equal original[3]"); + assert.equal(transformed[2].x, original[4], "Expect transform[2].x to equal original[4]"); + assert.equal(transformed[2].y, original[5], "Expect transform[2].y to equal original[5]"); + assert.equal(transformed[3].x, original[6], "Expect transform[3].x to equal original[6]"); + assert.equal(transformed[3].y, original[7], "Expect transform[3].y to equal original[7]"); + } + const originalLine1 = { + text: "line 1 text", + boundingBox: [1, 2, 3, 4, 5, 6, 7, 8], + words: [ + { + text: "word text 1", + boundingBox: [2, 3, 4, 5, 6, 7, 8, 9], + confidence: 0.9 + }, + { + text: "word text 2", + boundingBox: [3, 4, 5, 6, 7, 8, 9, 10], + confidence: 0.99 + } + ] + }; + + const pageNumber = 1; + + it("toTextLine() converts original TextLineModel", () => { + const transformed = toTextLine(originalLine1, pageNumber); + + assert.equal(transformed.kind, "line"); + assert.equal(transformed.pageNumber, pageNumber); + assert.deepStrictEqual(transformed.text, originalLine1.text); + verifyBoundingBox(transformed.boundingBox, originalLine1.boundingBox); + + assert.deepStrictEqual(transformed.words[0].kind, "word"); + assert.deepStrictEqual(transformed.words[0].text, originalLine1.words[0].text); + assert.deepStrictEqual(transformed.words[0].confidence, originalLine1.words[0].confidence); + assert.deepStrictEqual(transformed.words[0].pageNumber, pageNumber); + assert.deepStrictEqual(transformed.words[1].kind, "word"); + assert.deepStrictEqual(transformed.words[1].text, originalLine1.words[1].text); + assert.deepStrictEqual(transformed.words[1].confidence, originalLine1.words[1].confidence); + assert.deepStrictEqual(transformed.words[1].pageNumber, pageNumber); + + assert.equal(transformed.words[0].containingLine, transformed); + assert.equal(transformed.words[1].containingLine, transformed); + }); + + const originalLine2 = { + text: "line 2 text", + boundingBox: [1, 2, 3, 4, 5, 6, 7, 8], + words: [ + { + text: "word text 1", + boundingBox: [2, 3, 4, 5, 6, 7, 8, 9], + confidence: 0.9 + }, + { + text: "word text 2", + boundingBox: [3, 4, 5, 6, 7, 8, 9, 10], + confidence: 0.99 + } + ] + }; + + const originalReadResult1: ReadResultModel = { + pageNumber: 1, + angle: 0.2, + width: 100, + height: 200, + unit: "pixel", + lines: [originalLine1] + }; + + it("toRawExtractedPage() converts original ReadResultModel", () => { + const transformed = toFormPage(originalReadResult1); + + assert.equal(transformed.pageNumber, originalReadResult1.pageNumber); + assert.equal(transformed.angle, originalReadResult1.angle); + assert.equal(transformed.width, originalReadResult1.width); + assert.equal(transformed.height, originalReadResult1.height); + assert.equal(transformed.unit, originalReadResult1.unit); + }); + + const originalReadResult2: ReadResultModel = { + pageNumber: 1, + angle: 0.2, + width: 100, + height: 200, + unit: "pixel", + lines: [originalLine1, originalLine2] + }; + + it("toExtractedElement() converts word string reference to extracted word", () => { + const stringRef = "#/readResults/0/lines/0/words/0"; + const readResults = [originalReadResult1, originalReadResult2].map(toFormPage); + + const transformed = toFormElement(stringRef, readResults); + + assert.deepStrictEqual(transformed, readResults[0].lines![0].words[0]); + }); + + const rawExtractedPages = [originalReadResult1, originalReadResult2].map(toFormPage); + + it("toExtractedElement() converts line string reference to extracted line", () => { + const stringRef = "#/readResults/1/lines/1"; + + const transformed = toFormElement(stringRef, rawExtractedPages); + + assert.deepStrictEqual(transformed, rawExtractedPages[1].lines![1]); + }); + + const originalKeyValueElement1 = { + text: "keyvalue element text", + boundingBox: [1, 2, 3, 4, 5, 6, 7, 8], + elements: ["#/readResults/0/lines/0/words/0", "#/readResults/0/lines/0/words/1"] + }; + + it("toKeyValueElement() converts original KeyValueElementModel", () => { + const transformed = toFormText(originalKeyValueElement1, rawExtractedPages); + + assert.equal(transformed.text, originalKeyValueElement1.text); + assert.ok(transformed.boundingBox); + verifyBoundingBox(transformed.boundingBox!, originalKeyValueElement1.boundingBox); + assert.deepStrictEqual(transformed.elements![0], rawExtractedPages[0].lines![0].words[0]); + assert.deepStrictEqual(transformed.elements![1], rawExtractedPages[0].lines![0].words[1]); + }); + + it("toKeyValuePair() converts original key value pair", () => { + const original = { + label: "key value pair 1 label", + confidence: 0.999, + key: originalKeyValueElement1, + value: originalKeyValueElement1 + }; + + const transformed = toFormField(original, rawExtractedPages); + + assert.equal(transformed.label, original.label); + assert.equal(transformed.confidence, original.confidence); + assert.ok(transformed.fieldLabel.boundingBox); + assert.ok(transformed.valueText.boundingBox); + verifyBoundingBox(transformed.fieldLabel.boundingBox!, original.key.boundingBox); + verifyBoundingBox(transformed.valueText.boundingBox!, original.value.boundingBox); + assert.deepStrictEqual(transformed.fieldLabel.elements![0], rawExtractedPages[0].lines![0].words[0]); + assert.deepStrictEqual(transformed.valueText.elements![1], rawExtractedPages[0].lines![0].words[1]); + }); + + describe("toFieldValue()", () => { + const commonProperties = { + text: "field value text", + boudningBox: [1, 2, 3, 4, 5, 6, 7, 8], + confidence: 0.9999, + elements: ["#/readResults/0/lines/0/words/0", "#/readResults/0/lines/0/words/1"] + }; + + it("converts field value of string", () => { + const original: FieldValueModel = { + type: "string", + valueString: "string value", + ...commonProperties + }; + + const transformed = toFieldValue(original, rawExtractedPages); + + assert.equal(transformed.type, "string"); + assert.equal(transformed.value, original.valueString); + assert.equal((transformed as StringFieldValue).text, original.text); + }); + + it("converts field value of date", () => { + const original: FieldValueModel = { + type: "date", + valueDate: new Date(1999, 9, 9), + ...commonProperties + }; + + const transformed = toFieldValue(original, rawExtractedPages); + + assert.equal(transformed.type, "date"); + assert.equal(transformed.value, original.valueDate); + assert.equal((transformed as DateFieldValue).text, original.text); + }); + + it("converts field value of time", () => { + const original: FieldValueModel = { + type: "time", + valueTime: "time value", + ...commonProperties + }; + + const transformed = toFieldValue(original, rawExtractedPages); + + assert.equal(transformed.type, "time"); + assert.equal(transformed.value, original.valueTime); + assert.equal((transformed as TimeFieldValue).text, original.text); + }); + + it("converts field value of phoneNumber", () => { + const original: FieldValueModel = { + type: "phoneNumber", + valuePhoneNumber: "phoneNumber value", + ...commonProperties + }; + + const transformed = toFieldValue(original, rawExtractedPages); + + assert.equal(transformed.type, "phoneNumber"); + assert.equal(transformed.value, original.valuePhoneNumber); + assert.equal((transformed as PhoneNumberFieldValue).text, original.text); + }); + + it("converts field value of number", () => { + const original: FieldValueModel = { + type: "number", + valueNumber: 2.2, + ...commonProperties + }; + + const transformed = toFieldValue(original, rawExtractedPages); + + assert.equal(transformed.type, "number"); + assert.equal(transformed.value, original.valueNumber); + assert.equal((transformed as NumberFieldValue).text, original.text); + }); + + it("converts field value of integer", () => { + const original: FieldValueModel = { + type: "integer", + valueInteger: 1, + ...commonProperties + }; + + const transformed = toFieldValue(original, rawExtractedPages); + + assert.equal(transformed.type, "integer"); + assert.equal(transformed.value, original.valueInteger); + assert.equal((transformed as IntegerFieldValue).text, original.text); + }); + + it("converts field value of array", () => { + const originalDate: FieldValueModel = { + type: "date", + valueDate: new Date(1999, 9, 9), + ...commonProperties + }; + const originalInteger: FieldValueModel = { + type: "integer", + valueInteger: 1, + ...commonProperties + }; + const original: FieldValueModel = { + type: "array", + valueArray: [originalDate, originalInteger] + }; + + const transformed = toFieldValue(original, rawExtractedPages); + + assert.equal(transformed.type, "array"); + + const array = (transformed as ArrayFieldValue).value; + assert.equal(array![0].type, "date"); + assert.equal(array![0].value, originalDate.valueDate); + assert.equal((array![0] as DateFieldValue).text, originalDate.text); + assert.deepStrictEqual( + (array![0] as DateFieldValue).elements![0], + rawExtractedPages[0].lines![0].words[0] + ); + assert.equal(array![1].type, "integer"); + assert.equal(array![1].value, originalInteger.valueInteger); + assert.equal((array![1] as DateFieldValue).text, originalDate.text); + }); + + it("converts field value of object", () => { + const originalDate: FieldValueModel = { + type: "date", + valueDate: new Date(1999, 9, 9), + ...commonProperties + }; + const originalInteger: FieldValueModel = { + type: "integer", + valueInteger: 1, + ...commonProperties + }; + const original: FieldValueModel = { + type: "object", + valueObject: { + dateProperty: originalDate, + integerProperty: originalInteger + } + }; + + const transformed = toFieldValue(original, rawExtractedPages); + + assert.equal(transformed.type, "object"); + + const obj = transformed as ObjectFieldValue; + assert.equal(obj.type, "object"); + assert.equal(obj.value!["dateProperty"].value, originalDate.valueDate); + assert.equal(obj.value!["integerProperty"].value, originalInteger.valueInteger); + }); + }); + + it("toTable() converts original data table", () => { + const originalTable: DataTableModel = { + rows: 3, + columns: 2, + cells: [ + { + text: "r0c0", + columnIndex: 0, + confidence: 0.1, + rowIndex: 0, + boundingBox: [1, 2, 3, 4, 5, 6, 7, 8], + isHeader: true + }, + { + text: "r0c1", + columnIndex: 1, + confidence: 0.2, + rowIndex: 0, + boundingBox: [1, 2, 3, 4, 5, 6, 7, 8] + }, + { + text: "r1c0", + columnIndex: 0, + confidence: 0.3, + rowIndex: 1, + boundingBox: [1, 2, 3, 4, 5, 6, 7, 8] + }, + { + text: "r1c1", + columnIndex: 1, + confidence: 0.4, + rowIndex: 1, + boundingBox: [1, 2, 3, 4, 5, 6, 7, 8] + }, + { + text: "r2c0", + columnIndex: 0, + confidence: 0.3, + rowIndex: 2, + boundingBox: [1, 2, 3, 4, 5, 6, 7, 8], + columnSpan: 2, + isFooter: true + } + ] + }; + + const transformed = toFormTable(originalTable, rawExtractedPages); + + assert.equal(transformed.rows.length, originalTable.rows); + assert.equal(transformed.rows[0].cells[0].text, originalTable.cells[0].text); + assert.equal(transformed.rows[1].cells[1].confidence, originalTable.cells[3].confidence); + + assert.equal(transformed.rows[0].cells[0].isHeader, true); + assert.equal(transformed.rows[0].cells[0].isFooter, false); + assert.equal(transformed.rows[0].cells[0].rowSpan, 1); + assert.equal(transformed.rows[0].cells[0].columnSpan, 1); + + assert.equal(transformed.rows[2].cells[0].isHeader, false); + assert.equal(transformed.rows[2].cells[0].isFooter, true); + assert.equal(transformed.rows[2].cells[0].rowSpan, 1); + assert.equal(transformed.rows[2].cells[0].columnSpan, 2); + }); +}); diff --git a/sdk/formrecognizer/ai-form-recognizer/tsconfig.json b/sdk/formrecognizer/ai-form-recognizer/tsconfig.json new file mode 100644 index 000000000000..733a8ef32522 --- /dev/null +++ b/sdk/formrecognizer/ai-form-recognizer/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.package", + "compilerOptions": { + "outDir": "./dist-esm", + "declarationDir": "./types" + }, + "exclude": ["node_modules", "types", "temp", "browser", "dist", "dist-esm", "./samples/**/*.ts"] +}