diff --git a/.github/workflows/blob-publish.yml b/.github/workflows/blob-publish.yml new file mode 100644 index 000000000..a19e395cc --- /dev/null +++ b/.github/workflows/blob-publish.yml @@ -0,0 +1,70 @@ +name: Blob - Version and Release + +on: + workflow_dispatch: + inputs: + newversion: + type: choice + description: "Semantic Version Bump Type" + default: patch + options: + - patch + - minor + - major + +concurrency: + group: "push-to-main" + +defaults: + run: + working-directory: packages/blob + +jobs: + version_and_release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + # Needed to push the tag and the commit on the main branch, otherwise we get: + # > Run git push --follow-tags + # remote: error: GH006: Protected branch update failed for refs/heads/main. + # remote: error: Changes must be made through a pull request. Required status check "lint" is expected. + token: ${{ secrets.BOT_ACCESS_TOKEN }} + - run: corepack enable + - uses: actions/setup-node@v3 + with: + node-version: "20" + cache: "pnpm" + cache-dependency-path: | + packages/blob/pnpm-lock.yaml + # setting a registry enables the NODE_AUTH_TOKEN env variable where we can set an npm token. REQUIRED + registry-url: "https://registry.npmjs.org" + - run: pnpm install + - run: git config --global user.name machineuser + - run: git config --global user.email infra+machineuser@huggingface.co + - run: | + PACKAGE_VERSION=$(node -p "require('./package.json').version") + BUMPED_VERSION=$(node -p "require('semver').inc('$PACKAGE_VERSION', '${{ github.event.inputs.newversion }}')") + # Update package.json with the new version + node -e "const fs = require('fs'); const package = JSON.parse(fs.readFileSync('./package.json')); package.version = '$BUMPED_VERSION'; fs.writeFileSync('./package.json', JSON.stringify(package, null, '\t') + '\n');" + git commit . -m "🔖 @huggingface/blob $BUMPED_VERSION" + git tag "blob-v$BUMPED_VERSION" + + - run: pnpm publish --no-git-checks . + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: git pull --rebase && git push --follow-tags + # hack - reuse actions/setup-node@v3 just to set a new registry + - uses: actions/setup-node@v3 + with: + node-version: "20" + registry-url: "https://npm.pkg.github.com" + # Disable for now, until github supports PATs for writing github packages (https://github.com/github/roadmap/issues/558) + # - run: pnpm publish --no-git-checks . + # env: + # NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Update Doc" + uses: peter-evans/repository-dispatch@v2 + with: + event-type: doc-build + token: ${{ secrets.BOT_ACCESS_TOKEN }} diff --git a/.github/workflows/dduf-publish.yml b/.github/workflows/dduf-publish.yml new file mode 100644 index 000000000..8d543d03e --- /dev/null +++ b/.github/workflows/dduf-publish.yml @@ -0,0 +1,73 @@ +name: DDUF - Version and Release + +on: + workflow_dispatch: + inputs: + newversion: + type: choice + description: "Semantic Version Bump Type" + default: patch + options: + - patch + - minor + - major + +concurrency: + group: "push-to-main" + +defaults: + run: + working-directory: packages/dduf + +jobs: + version_and_release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + # Needed to push the tag and the commit on the main branch, otherwise we get: + # > Run git push --follow-tags + # remote: error: GH006: Protected branch update failed for refs/heads/main. + # remote: error: Changes must be made through a pull request. Required status check "lint" is expected. + token: ${{ secrets.BOT_ACCESS_TOKEN }} + - run: corepack enable + - uses: actions/setup-node@v3 + with: + node-version: "20" + cache: "pnpm" + cache-dependency-path: | + packages/dduf/pnpm-lock.yaml + # setting a registry enables the NODE_AUTH_TOKEN env variable where we can set an npm token. REQUIRED + registry-url: "https://registry.npmjs.org" + - run: pnpm install + - run: git config --global user.name machineuser + - run: git config --global user.email infra+machineuser@huggingface.co + - run: | + PACKAGE_VERSION=$(node -p "require('./package.json').version") + BUMPED_VERSION=$(node -p "require('semver').inc('$PACKAGE_VERSION', '${{ github.event.inputs.newversion }}')") + # Update package.json with the new version + node -e "const fs = require('fs'); const package = JSON.parse(fs.readFileSync('./package.json')); package.version = '$BUMPED_VERSION'; fs.writeFileSync('./package.json', JSON.stringify(package, null, '\t') + '\n');" + git commit . -m "🔖 @huggingface/dduf $BUMPED_VERSION" + git tag "dduf-v$BUMPED_VERSION" + + - name: "Check Deps are published before publishing this package" + run: pnpm -w check-deps blob + + - run: pnpm publish --no-git-checks . + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: git pull --rebase && git push --follow-tags + # hack - reuse actions/setup-node@v3 just to set a new registry + - uses: actions/setup-node@v3 + with: + node-version: "20" + registry-url: "https://npm.pkg.github.com" + # Disable for now, until github supports PATs for writing github packages (https://github.com/github/roadmap/issues/558) + # - run: pnpm publish --no-git-checks . + # env: + # NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Update Doc" + uses: peter-evans/repository-dispatch@v2 + with: + event-type: doc-build + token: ${{ secrets.BOT_ACCESS_TOKEN }} diff --git a/.github/workflows/gguf-publish.yml b/.github/workflows/gguf-publish.yml index f4791ac5d..383d98b47 100644 --- a/.github/workflows/gguf-publish.yml +++ b/.github/workflows/gguf-publish.yml @@ -49,6 +49,10 @@ jobs: node -e "const fs = require('fs'); const package = JSON.parse(fs.readFileSync('./package.json')); package.version = '$BUMPED_VERSION'; fs.writeFileSync('./package.json', JSON.stringify(package, null, '\t') + '\n');" git commit . -m "🔖 @huggingface/gguf $BUMPED_VERSION" git tag "gguf-v$BUMPED_VERSION" + + - name: "Check Deps are published before publishing this package" + run: pnpm -w check-deps tasks + - run: pnpm publish --no-git-checks . env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/hub-publish.yml b/.github/workflows/hub-publish.yml index f00c0613e..d65636020 100644 --- a/.github/workflows/hub-publish.yml +++ b/.github/workflows/hub-publish.yml @@ -53,42 +53,8 @@ jobs: git commit -m "🔖 @huggingface/hub $BUMPED_VERSION" git tag "hub-v$BUMPED_VERSION" - - name: Make sure that the latest version of @huggingface/tasks is consistent with the local version - run: | - LOCAL_TASKS_VERSION=$(node -p "require('./package.json').version") - REMOTE_TASKS_VERSION=$(npm view @huggingface/tasks version) - - # If the versions are different, error - if [ "$LOCAL_TASKS_VERSION" != "$REMOTE_TASKS_VERSION" ]; then - echo "Error: The local @huggingface/tasks package version ($LOCAL_TASKS_VERSION) differs from the remote version ($REMOTE_TASKS_VERSION). Release halted." - exit 1 - fi - - npm pack @huggingface/tasks - mv huggingface-tasks-$LOCAL_TASKS_VERSION.tgz tasks-local.tgz - - npm pack @huggingface/tasks@$REMOTE_TASKS_VERSION - mv huggingface-tasks-$REMOTE_TASKS_VERSION.tgz tasks-remote.tgz - - # Compute checksum of local tar. We need to extract both tar since the remote compression might be different - tar -xf tasks-local.tgz - LOCAL_CHECKSUM=$(cd package && tar --mtime='1970-01-01' --mode=755 -cf - . | sha256sum | cut -d' ' -f1) - echo "Local package checksum: $LOCAL_CHECKSUM" - - rm -Rf package - - tar -xf tasks-remote.tgz - REMOTE_CHECKSUM=$(cd package && tar --mtime='1970-01-01' --mode=755 -cf - . | sha256sum | cut -d' ' -f1) - echo "Remote package checksum: $REMOTE_CHECKSUM" - - rm -Rf package - - if [ "$LOCAL_CHECKSUM" != "$REMOTE_CHECKSUM" ]; then - echo "Checksum Verification Failed: The local @huggingface/tasks package differs from the remote version. Release halted. Local Checksum: $LOCAL_CHECKSUM, Remote Checksum: $REMOTE_CHECKSUM" - exit 1 - fi - echo "Checksum Verification Successful: The local and remote @huggingface/tasks packages are consistent. Proceeding with the @huggingface/widgets package release. Local Checksum: $LOCAL_CHECKSUM, Remote Checksum: $REMOTE_CHECKSUM." - working-directory: packages/tasks + - name: "Check Deps are published before publishing this package" + run: pnpm -w check-deps tasks - run: pnpm publish --no-git-checks . env: diff --git a/.github/workflows/inference-publish.yml b/.github/workflows/inference-publish.yml index ee346c24c..779f62976 100644 --- a/.github/workflows/inference-publish.yml +++ b/.github/workflows/inference-publish.yml @@ -53,42 +53,8 @@ jobs: git commit -m "🔖 @huggingface/inference $BUMPED_VERSION" git tag "inference-v$BUMPED_VERSION" - - name: Make sure that the latest version of @huggingface/tasks is consistent with the local version - run: | - LOCAL_TASKS_VERSION=$(node -p "require('./package.json').version") - REMOTE_TASKS_VERSION=$(npm view @huggingface/tasks version) - - # If the versions are different, error - if [ "$LOCAL_TASKS_VERSION" != "$REMOTE_TASKS_VERSION" ]; then - echo "Error: The local @huggingface/tasks package version ($LOCAL_TASKS_VERSION) differs from the remote version ($REMOTE_TASKS_VERSION). Release halted." - exit 1 - fi - - npm pack @huggingface/tasks - mv huggingface-tasks-$LOCAL_TASKS_VERSION.tgz tasks-local.tgz - - npm pack @huggingface/tasks@$REMOTE_TASKS_VERSION - mv huggingface-tasks-$REMOTE_TASKS_VERSION.tgz tasks-remote.tgz - - # Compute checksum of local tar. We need to extract both tar since the remote compression might be different - tar -xf tasks-local.tgz - LOCAL_CHECKSUM=$(cd package && tar --mtime='1970-01-01' --mode=755 -cf - . | sha256sum | cut -d' ' -f1) - echo "Local package checksum: $LOCAL_CHECKSUM" - - rm -Rf package - - tar -xf tasks-remote.tgz - REMOTE_CHECKSUM=$(cd package && tar --mtime='1970-01-01' --mode=755 -cf - . | sha256sum | cut -d' ' -f1) - echo "Remote package checksum: $REMOTE_CHECKSUM" - - rm -Rf package - - if [ "$LOCAL_CHECKSUM" != "$REMOTE_CHECKSUM" ]; then - echo "Checksum Verification Failed: The local @huggingface/tasks package differs from the remote version. Release halted. Local Checksum: $LOCAL_CHECKSUM, Remote Checksum: $REMOTE_CHECKSUM" - exit 1 - fi - echo "Checksum Verification Successful: The local and remote @huggingface/tasks packages are consistent. Proceeding with the @huggingface/widgets package release. Local Checksum: $LOCAL_CHECKSUM, Remote Checksum: $REMOTE_CHECKSUM." - working-directory: packages/tasks + - name: "Check Deps are published before publishing this package" + run: pnpm -w check-deps tasks - run: pnpm publish --no-git-checks . env: diff --git a/.github/workflows/tasks-publish.yml b/.github/workflows/tasks-publish.yml index dbee121ea..dca33c507 100644 --- a/.github/workflows/tasks-publish.yml +++ b/.github/workflows/tasks-publish.yml @@ -50,43 +50,6 @@ jobs: git commit . -m "🔖 @huggingface/tasks $BUMPED_VERSION" git tag "tasks-v$BUMPED_VERSION" - - name: Make sure that the latest version of @huggingface/gguf is consistent with the local version - run: | - LOCAL_GGUF_VERSION=$(node -p "require('./package.json').version") - REMOTE_GGUF_VERSION=$(npm view @huggingface/gguf version) - - # If the versions are different, error - if [ "$LOCAL_GGUF_VERSION" != "$REMOTE_GGUF_VERSION" ]; then - echo "Error: The local @huggingface/gguf package version ($LOCAL_GGUF_VERSION) differs from the remote version ($REMOTE_GGUF_VERSION). Release halted." - exit 1 - fi - - npm pack @huggingface/gguf - mv huggingface-gguf-$LOCAL_GGUF_VERSION.tgz gguf-local.tgz - - npm pack @huggingface/gguf@$REMOTE_GGUF_VERSION - mv huggingface-gguf-$REMOTE_GGUF_VERSION.tgz gguf-remote.tgz - - # Compute checksum of local tar. We need to extract both tar since the remote compression might be different - tar -xf gguf-local.tgz - LOCAL_CHECKSUM=$(cd package && tar --mtime='1970-01-01' --mode=755 -cf - . | sha256sum | cut -d' ' -f1) - echo "Local package checksum: $LOCAL_CHECKSUM" - - rm -Rf package - - tar -xf gguf-remote.tgz - REMOTE_CHECKSUM=$(cd package && tar --mtime='1970-01-01' --mode=755 -cf - . | sha256sum | cut -d' ' -f1) - echo "Remote package checksum: $REMOTE_CHECKSUM" - - rm -Rf package - - if [ "$LOCAL_CHECKSUM" != "$REMOTE_CHECKSUM" ]; then - echo "Checksum Verification Failed: The local @huggingface/gguf package differs from the remote version. Release halted. Local Checksum: $LOCAL_CHECKSUM, Remote Checksum: $REMOTE_CHECKSUM" - exit 1 - fi - echo "Checksum Verification Successful: The local and remote @huggingface/gguf packages are consistent. Proceeding with the @huggingface/widgets package release. Local Checksum: $LOCAL_CHECKSUM, Remote Checksum: $REMOTE_CHECKSUM." - working-directory: packages/gguf - - run: pnpm publish --no-git-checks . env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eea042652..28a657d82 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -104,8 +104,8 @@ jobs: working-directory: e2e run: | sleep 3 - pnpm i --filter root --filter inference... --filter hub... --frozen-lockfile - pnpm --filter inference --filter hub --filter tasks --filter gguf publish --force --no-git-checks --registry http://localhost:4874/ + pnpm i --filter root --filter inference... --filter hub... --filter tasks-gen --frozen-lockfile + pnpm --filter inference --filter hub --filter tasks publish --force --no-git-checks --registry http://localhost:4874/ - name: E2E test - test yarn install working-directory: e2e/ts diff --git a/.github/workflows/update-specs.yml b/.github/workflows/update-specs.yml new file mode 100644 index 000000000..d5d31decb --- /dev/null +++ b/.github/workflows/update-specs.yml @@ -0,0 +1,63 @@ +name: Tasks - Update specs + +on: + workflow_dispatch: + schedule: + - cron: "0 3 * * *" # Every day at 3am + +concurrency: + group: update-specs + cancel-in-progress: true + +defaults: + run: + working-directory: packages/tasks-gen + +jobs: + pull_request: + runs-on: ubuntu-latest + steps: + # Setup + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: "20" + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + run_install: true + + # Generate specs + - run: pnpm run inference-tgi-import + - run: pnpm run inference-tei-import + - run: pnpm run inference-codegen + + # Check changes + - run: git status + + # Create or update Pull Request + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.TOKEN_INFERENCE_SYNC_BOT }} + commit-message: Update tasks specs (automated commit) + branch: update-tasks-specs-automated-pr + delete-branch: true + title: "[Bot] Update tasks specs" + body: | + This PR updates the @huggingface/tasks specs. It has been generated by running: + ```sh + pnpm run inference-tgi-import + pnpm run inference-tei-import + pnpm run inference-codegen + ``` + + This PR was automatically created by the [Tasks - Update specs workflow](https://github.com/huggingface/huggingface.js/blob/main/.github/update-specs.yml). + + Make sure the changes are correct before merging. + labels: | + tasks + specs + reviewers: | + Wauplin + hanouticelina diff --git a/.github/workflows/widgets-publish.yml b/.github/workflows/widgets-publish.yml deleted file mode 100644 index 502ffebe5..000000000 --- a/.github/workflows/widgets-publish.yml +++ /dev/null @@ -1,176 +0,0 @@ -name: Widgets - Version and Release - -on: - workflow_dispatch: - inputs: - newversion: - type: choice - description: "Semantic Version Bump Type" - default: patch - options: - - patch - - minor - - major - -defaults: - run: - working-directory: packages/widgets - -concurrency: - group: "push-to-main" - -jobs: - version_and_release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - # Needed to push the tag and the commit on the main branch, otherwise we get: - # > Run git push --follow-tags - # remote: error: GH006: Protected branch update failed for refs/heads/main. - # remote: error: Changes must be made through a pull request. Required status check "lint" is expected. - token: ${{ secrets.BOT_ACCESS_TOKEN }} - - run: corepack enable - - uses: actions/setup-node@v3 - with: - node-version: "20" - cache: "pnpm" - cache-dependency-path: | - packages/widgets/pnpm-lock.yaml - # setting a registry enables the NODE_AUTH_TOKEN env variable where we can set an npm token. REQUIRED - registry-url: "https://registry.npmjs.org" - - run: pnpm install - - run: git config --global user.name machineuser - - run: git config --global user.email infra+machineuser@huggingface.co - - run: | - PACKAGE_VERSION=$(node -p "require('./package.json').version") - BUMPED_VERSION=$(node -p "require('semver').inc('$PACKAGE_VERSION', '${{ github.event.inputs.newversion }}')") - # Update package.json with the new version - node -e "const fs = require('fs'); const package = JSON.parse(fs.readFileSync('./package.json')); package.version = '$BUMPED_VERSION'; fs.writeFileSync('./package.json', JSON.stringify(package, null, '\t') + '\n');" - git commit . -m "🔖 @huggingface/widgets $BUMPED_VERSION" - git tag "widgets-v$BUMPED_VERSION" - - - name: Make sure that the latest version of @huggingface/tasks is consistent with the local version - run: | - LOCAL_TASKS_VERSION=$(node -p "require('./package.json').version") - REMOTE_TASKS_VERSION=$(npm view @huggingface/tasks version) - - # If the versions are different, error - if [ "$LOCAL_TASKS_VERSION" != "$REMOTE_TASKS_VERSION" ]; then - echo "Error: The local @huggingface/tasks package version ($LOCAL_TASKS_VERSION) differs from the remote version ($REMOTE_TASKS_VERSION). Release halted." - exit 1 - fi - - npm pack @huggingface/tasks - mv huggingface-tasks-$LOCAL_TASKS_VERSION.tgz tasks-local.tgz - - npm pack @huggingface/tasks@$REMOTE_TASKS_VERSION - mv huggingface-tasks-$REMOTE_TASKS_VERSION.tgz tasks-remote.tgz - - # Compute checksum of local tar. We need to extract both tar since the remote compression might be different - tar -xf tasks-local.tgz - LOCAL_CHECKSUM=$(cd package && tar --mtime='1970-01-01' --mode=755 -cf - . | sha256sum | cut -d' ' -f1) - echo "Local package checksum: $LOCAL_CHECKSUM" - - rm -Rf package - - tar -xf tasks-remote.tgz - REMOTE_CHECKSUM=$(cd package && tar --mtime='1970-01-01' --mode=755 -cf - . | sha256sum | cut -d' ' -f1) - echo "Remote package checksum: $REMOTE_CHECKSUM" - - rm -Rf package - - if [ "$LOCAL_CHECKSUM" != "$REMOTE_CHECKSUM" ]; then - echo "Checksum Verification Failed: The local @huggingface/tasks package differs from the remote version. Release halted. Local Checksum: $LOCAL_CHECKSUM, Remote Checksum: $REMOTE_CHECKSUM" - exit 1 - fi - echo "Checksum Verification Successful: The local and remote @huggingface/tasks packages are consistent. Proceeding with the @huggingface/widgets package release. Local Checksum: $LOCAL_CHECKSUM, Remote Checksum: $REMOTE_CHECKSUM." - working-directory: packages/tasks - - - name: Make sure that the latest version of @huggingface/jinja is consistent with the local version - run: | - LOCAL_JINJA_VERSION=$(node -p "require('./package.json').version") - REMOTE_JINJA_VERSION=$(npm view @huggingface/jinja version) - - # If the versions are different, error - if [ "$LOCAL_JINJA_VERSION" != "$REMOTE_JINJA_VERSION" ]; then - echo "Error: The local @huggingface/jinja package version ($LOCAL_JINJA_VERSION) differs from the remote version ($REMOTE_JINJA_VERSION). Release halted." - exit 1 - fi - - npm pack @huggingface/jinja - mv huggingface-jinja-$LOCAL_JINJA_VERSION.tgz jinja-local.tgz - - npm pack @huggingface/jinja@$REMOTE_JINJA_VERSION - mv huggingface-jinja-$REMOTE_JINJA_VERSION.tgz jinja-remote.tgz - - # Compute checksum of local tar. We need to extract both tar since the remote compression might be different - tar -xf jinja-local.tgz - LOCAL_CHECKSUM=$(cd package && tar --mtime='1970-01-01' --mode=755 -cf - . | sha256sum | cut -d' ' -f1) - echo "Local package checksum: $LOCAL_CHECKSUM" - - rm -Rf package - - tar -xf jinja-remote.tgz - REMOTE_CHECKSUM=$(cd package && tar --mtime='1970-01-01' --mode=755 -cf - . | sha256sum | cut -d' ' -f1) - echo "Remote package checksum: $REMOTE_CHECKSUM" - - rm -Rf package - - if [ "$LOCAL_CHECKSUM" != "$REMOTE_CHECKSUM" ]; then - echo "Checksum Verification Failed: The local @huggingface/jinja package differs from the remote version. Release halted. Local Checksum: $LOCAL_CHECKSUM, Remote Checksum: $REMOTE_CHECKSUM" - exit 1 - fi - echo "Checksum Verification Successful: The local and remote @huggingface/jinja packages are consistent. Proceeding with the @huggingface/widgets package release. Local Checksum: $LOCAL_CHECKSUM, Remote Checksum: $REMOTE_CHECKSUM." - working-directory: packages/jinja - - - name: Make sure that the latest version of @huggingface/inference is consistent with the local version - run: | - LOCAL_INFERENCE_VERSION=$(node -p "require('./package.json').version") - REMOTE_INFERENCE_VERSION=$(npm view @huggingface/inference version) - - # If the versions are different, error - if [ "$LOCAL_INFERENCE_VERSION" != "$REMOTE_INFERENCE_VERSION" ]; then - echo "Error: The local @huggingface/inference package version ($LOCAL_INFERENCE_VERSION) differs from the remote version ($REMOTE_INFERENCE_VERSION). Release halted." - exit 1 - fi - - npm pack @huggingface/inference - mv huggingface-inference-$LOCAL_INFERENCE_VERSION.tgz inference-local.tgz - - npm pack @huggingface/inference@$REMOTE_INFERENCE_VERSION - mv huggingface-inference-$REMOTE_INFERENCE_VERSION.tgz inference-remote.tgz - - # Compute checksum of local tar. We need to extract both tar since the remote compression might be different - tar -xf inference-local.tgz - LOCAL_CHECKSUM=$(cd package && tar --mtime='1970-01-01' --mode=755 -cf - . | sha256sum | cut -d' ' -f1) - echo "Local package checksum: $LOCAL_CHECKSUM" - - rm -Rf package - - tar -xf inference-remote.tgz - REMOTE_CHECKSUM=$(cd package && tar --mtime='1970-01-01' --mode=755 -cf - . | sha256sum | cut -d' ' -f1) - echo "Remote package checksum: $REMOTE_CHECKSUM" - - rm -Rf package - - if [ "$LOCAL_CHECKSUM" != "$REMOTE_CHECKSUM" ]; then - echo "Checksum Verification Failed: The local @huggingface/inference package differs from the remote version. Release halted. Local Checksum: $LOCAL_CHECKSUM, Remote Checksum: $REMOTE_CHECKSUM" - exit 1 - fi - echo "Checksum Verification Successful: The local and remote @huggingface/inference packages are consistent. Proceeding with the @huggingface/widgets package release. Local Checksum: $LOCAL_CHECKSUM, Remote Checksum: $REMOTE_CHECKSUM." - working-directory: packages/inference - - - run: pnpm publish --no-git-checks . - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - run: git pull --rebase && git push --follow-tags - # hack - reuse actions/setup-node@v3 just to set a new registry - - uses: actions/setup-node@v3 - with: - node-version: "20" - registry-url: "https://npm.pkg.github.com" - # Disable for now, until github supports PATs for writing github packages (https://github.com/github/roadmap/issues/558) - # - run: pnpm publish --no-git-checks . - # env: - # NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CODEOWNERS b/CODEOWNERS index 80148fb6c..bbcb48eb7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,7 +4,7 @@ # Ownership for the Tasks Package -/packages/tasks/ @osanseviero @SBrandeis @gary149 @Wauplin @julien-c @pcuenca +/packages/tasks/ @SBrandeis @gary149 @Wauplin @julien-c @pcuenca @ngxson # Ownership for the Hub Package @@ -20,7 +20,7 @@ # Ownership for the gguf Package -/packages/gguf @mishig25 @julien-c +/packages/gguf @mishig25 @ngxson @julien-c # Ownership for the space-header Package /packages/space-header @enzostvs diff --git a/README.md b/README.md index fc0c9a39e..838f80045 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ You can run our packages with vanilla JS, without any bundler, by using a CDN or ```html ``` diff --git a/package.json b/package.json index 1ffa5306f..687477c39 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,13 @@ "lint": "eslint --quiet --fix --ext .cjs,.ts .eslintrc.cjs", "lint:check": "eslint --ext .cjs,.ts .eslintrc.cjs", "format": "prettier --write package.json .prettierrc .vscode .eslintrc.cjs e2e .github *.md", - "format:check": "prettier --check package.json .prettierrc .vscode .eslintrc.cjs .github *.md" + "format:check": "prettier --check package.json .prettierrc .vscode .eslintrc.cjs .github *.md", + "check-deps": "tsx scripts/check-deps.ts" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", + "@types/node": "^18.16.1", "@vitest/browser": "^0.34.6", "eslint": "^8.57.0", "eslint-config-prettier": "^9.0.0", @@ -21,6 +23,7 @@ "prettier": "^3.1.0", "prettier-plugin-svelte": "^3.1.2", "semver": "^7.5.0", + "tshy": "^3.0.2", "tsup": "^6.7.0", "tsx": "^4.7.0", "typescript": "^5.4.2", diff --git a/packages/blob/.eslintignore b/packages/blob/.eslintignore new file mode 100644 index 000000000..67c4f0d9d --- /dev/null +++ b/packages/blob/.eslintignore @@ -0,0 +1,2 @@ +dist +sha256.js diff --git a/packages/blob/.prettierignore b/packages/blob/.prettierignore new file mode 100644 index 000000000..4fafcf634 --- /dev/null +++ b/packages/blob/.prettierignore @@ -0,0 +1,5 @@ +pnpm-lock.yaml +# In order to avoid code samples to have tabs, they don't display well on npm +README.md +dist +sha256.js \ No newline at end of file diff --git a/packages/blob/LICENSE b/packages/blob/LICENSE new file mode 100644 index 000000000..5e9693e35 --- /dev/null +++ b/packages/blob/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Hugging Face + +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/packages/blob/README.md b/packages/blob/README.md new file mode 100644 index 000000000..84ebce19c --- /dev/null +++ b/packages/blob/README.md @@ -0,0 +1,90 @@ +# 🤗 Hugging Face Blobs + +Utilities to convert a string or URL to a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) object, whether it represents a local file or a remote URL. + +`fetch` already returns a `Blob` object for remote URLs, but it loads the entire file in memory. This utility makes ad-hoc http range requests when calling `.slice()` on the blob. + +## Install + +```console +pnpm add @huggingface/blob + +npm add @huggingface/blob + +yarn add @huggingface/blob +``` + +### Deno + +```ts +// esm.sh +import { FileBlob } from "https://esm.sh/@huggingface/blob/FileBlob"; +import { WebBlob } from "https://esm.sh/@huggingface/blob/WebBlob"; +import { createBlob } from "https://esm.sh/@huggingface/blob"; +// or npm: +import { FileBlob } from "npm:@huggingface/blob/FileBlob"; +import { WebBlob } from "npm:@huggingface/blob/WebBlob"; +import { createBlob } from "npm:@huggingface/blob"; +``` + +## Usage + + +```ts +import { FileBlob } from "@huggingface/blob/FileBlob"; +import { WebBlob } from "@huggingface/blob/WebBlob"; +import { createBlob } from "@huggingface/blob"; + +const fileBlob = await FileBlob.create("path/to/file"); +const webBlob = await WebBlob.create("https://url/to/file"); + +const blob = await createBlob("..."); // Automatically detects if it's a file or web URL +``` + +## API + +### createBlob + +Creates a Blob object from a string or URL. Automatically detects if it's a file or web URL. + +```ts +await createBlob("...", { + /** + * Custom fetch function to use, in case it resolves to a Web Blob. + * + * Useful for adding headers, etc. + */ + fetch: ..., +}); + +### FileBlob + +```ts +await FileBlob.create("path/to/file"); +await FileBlob.create(new URL("file:///path/to/file")); +``` + +### WebBlob + +Creates a Blob object from a URL. If the file is less than 1MB (as indicated by the Content-Length header), by default it will be cached in memory in entirety upon blob creation. + +This class is useful for large files that do not need to be loaded all at once in memory, as it makes range requests for the data. + +```ts +await WebBlob.create("https://url/to/file"); +await WebBlob.create(new URL("https://url/to/file")); + +await WebBlob.create("https://url/to/file", { + /** + * Custom fetch function to use. Useful for adding headers, etc. + */ + fetch: ..., + /** + * If the file is less than the specified size, it will be cached in memory in entirety upon blob creation, + * instead of doing range requests for the data. + * + * @default 1_000_000 + */ + cacheBelow: ... +}) +``` \ No newline at end of file diff --git a/packages/blob/index.ts b/packages/blob/index.ts new file mode 100644 index 000000000..e910bb060 --- /dev/null +++ b/packages/blob/index.ts @@ -0,0 +1 @@ +export * from "./src/index"; diff --git a/packages/blob/package.json b/packages/blob/package.json new file mode 100644 index 000000000..0bbdab75d --- /dev/null +++ b/packages/blob/package.json @@ -0,0 +1,64 @@ +{ + "name": "@huggingface/blob", + "packageManager": "pnpm@8.10.5", + "version": "0.0.2", + "description": "Utilities to convert URLs and files to Blobs, internally used by Hugging Face libs", + "repository": "https://github.com/huggingface/huggingface.js.git", + "publishConfig": { + "access": "public" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "require": "./dist/index.js", + "import": "./dist/index.mjs" + }, + "./package.json": "./package.json", + "./WebBlob": { + "require": "./dist/src/utils/WebBlob.js", + "import": "./dist/src/utils/WebBlob.mjs" + }, + "./FileBlob": { + "require": "./dist/src/utils/FileBlob.js", + "import": "./dist/src/utils/FileBlob.mjs" + } + }, + "browser": { + "./src/utils/FileBlob.ts": false, + "./dist/index.js": "./dist/browser/index.js", + "./dist/index.mjs": "./dist/browser/index.mjs" + }, + "source": "index.ts", + "scripts": { + "lint": "eslint --quiet --fix --ext .cjs,.ts .", + "lint:check": "eslint --ext .cjs,.ts .", + "format": "prettier --write .", + "format:check": "prettier --check .", + "prepublishOnly": "pnpm run build", + "build": "tsup && tsc --emitDeclarationOnly --declaration && cp dist/index.d.ts dist/index.m.d.ts && cp dist/src/utils/FileBlob.d.ts dist/src/utils/FileBlob.m.d.ts && cp dist/src/utils/WebBlob.d.ts dist/src/utils/WebBlob.m.d.ts", + "prepare": "pnpm run build", + "test": "vitest run", + "test:browser": "vitest run --browser.name=chrome --browser.headless --config vitest-browser.config.mts", + "check": "tsc" + }, + "files": [ + "src", + "dist", + "index.ts", + "tsconfig.json" + ], + "keywords": [ + "huggingface", + "hugging", + "face", + "blob", + "lazy" + ], + "author": "Hugging Face", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.11.28" + } +} diff --git a/packages/blob/pnpm-lock.yaml b/packages/blob/pnpm-lock.yaml new file mode 100644 index 000000000..f8939bbbb --- /dev/null +++ b/packages/blob/pnpm-lock.yaml @@ -0,0 +1,22 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + '@types/node': + specifier: ^20.11.28 + version: 20.11.28 + +packages: + + /@types/node@20.11.28: + resolution: {integrity: sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==} + dependencies: + undici-types: 5.26.5 + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true diff --git a/packages/blob/src/index.ts b/packages/blob/src/index.ts new file mode 100644 index 000000000..eadd56ab8 --- /dev/null +++ b/packages/blob/src/index.ts @@ -0,0 +1 @@ +export { createBlob } from "./utils/createBlob"; diff --git a/packages/blob/src/utils/FileBlob.spec.ts b/packages/blob/src/utils/FileBlob.spec.ts new file mode 100644 index 000000000..2ed51d8e3 --- /dev/null +++ b/packages/blob/src/utils/FileBlob.spec.ts @@ -0,0 +1,45 @@ +import { open, stat } from "node:fs/promises"; +import { TextDecoder } from "node:util"; +import { describe, expect, it } from "vitest"; +import { FileBlob } from "./FileBlob"; + +describe("FileBlob", () => { + it("should create a FileBlob with a slice on the entire file", async () => { + const file = await open("package.json", "r"); + const { size } = await stat("package.json"); + + const fileBlob = await FileBlob.create("package.json"); + + expect(fileBlob).toMatchObject({ + path: "package.json", + start: 0, + end: size, + }); + expect(fileBlob.size).toBe(size); + expect(fileBlob.type).toBe(""); + const text = await fileBlob.text(); + const expectedText = (await file.read(Buffer.alloc(size), 0, size)).buffer.toString("utf8"); + expect(text).toBe(expectedText); + const result = await fileBlob.stream().getReader().read(); + expect(new TextDecoder().decode(result.value)).toBe(expectedText); + }); + + it("should create a slice on the file", async () => { + const file = await open("package.json", "r"); + const fileBlob = await FileBlob.create("package.json"); + + const slice = fileBlob.slice(10, 20); + + expect(slice).toMatchObject({ + path: "package.json", + start: 10, + end: 20, + }); + expect(slice.size).toBe(10); + const sliceText = await slice.text(); + const expectedText = (await file.read(Buffer.alloc(10), 0, 10, 10)).buffer.toString("utf8"); + expect(sliceText).toBe(expectedText); + const result = await slice.stream().getReader().read(); + expect(new TextDecoder().decode(result.value)).toBe(expectedText); + }); +}); diff --git a/packages/blob/src/utils/FileBlob.ts b/packages/blob/src/utils/FileBlob.ts new file mode 100644 index 000000000..e783ca6fa --- /dev/null +++ b/packages/blob/src/utils/FileBlob.ts @@ -0,0 +1,118 @@ +import { createReadStream } from "node:fs"; +import { open, stat } from "node:fs/promises"; +import { Readable } from "node:stream"; +import type { FileHandle } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +/** + * @internal + * + * A FileBlob is a replacement for the Blob class that allows to lazy read files + * in order to preserve memory. + * + * It is a drop-in replacement for the Blob class, so you can use it as a Blob. + * + * The main difference is the instantiation, which is done asynchronously using the `FileBlob.create` method. + * + * @example + * const fileBlob = await FileBlob.create("path/to/package.json"); + * + * await fetch("https://aschen.tech", { method: "POST", body: fileBlob }); + */ +export class FileBlob extends Blob { + /** + * Creates a new FileBlob on the provided file. + * + * @param path Path to the file to be lazy readed + */ + static async create(path: string | URL): Promise { + path = path instanceof URL ? fileURLToPath(path) : path; + + const { size } = await stat(path); + + const fileBlob = new FileBlob(path, 0, size); + + return fileBlob; + } + + private path: string; + private start: number; + private end: number; + + private constructor(path: string, start: number, end: number) { + super(); + + this.path = path; + this.start = start; + this.end = end; + } + + /** + * Returns the size of the blob. + */ + override get size(): number { + return this.end - this.start; + } + + /** + * Returns a new instance of FileBlob that is a slice of the current one. + * + * The slice is inclusive of the start and exclusive of the end. + * + * The slice method does not supports negative start/end. + * + * @param start beginning of the slice + * @param end end of the slice + */ + override slice(start = 0, end = this.size): FileBlob { + if (start < 0 || end < 0) { + new TypeError("Unsupported negative start/end on FileBlob.slice"); + } + + const slice = new FileBlob(this.path, this.start + start, Math.min(this.start + end, this.end)); + + return slice; + } + + /** + * Read the part of the file delimited by the FileBlob and returns it as an ArrayBuffer. + */ + override async arrayBuffer(): Promise { + const slice = await this.execute((file) => file.read(Buffer.alloc(this.size), 0, this.size, this.start)); + + return slice.buffer; + } + + /** + * Read the part of the file delimited by the FileBlob and returns it as a string. + */ + override async text(): Promise { + const buffer = (await this.arrayBuffer()) as Buffer; + + return buffer.toString("utf8"); + } + + /** + * Returns a stream around the part of the file delimited by the FileBlob. + */ + override stream(): ReturnType { + return Readable.toWeb(createReadStream(this.path, { start: this.start, end: this.end - 1 })) as ReturnType< + Blob["stream"] + >; + } + + /** + * We are opening and closing the file for each action to prevent file descriptor leaks. + * + * It is an intended choice of developer experience over performances. + */ + private async execute(action: (file: FileHandle) => Promise) { + const file = await open(this.path, "r"); + + try { + return await action(file); + } finally { + await file.close(); + } + } +} diff --git a/packages/blob/src/utils/WebBlob.spec.ts b/packages/blob/src/utils/WebBlob.spec.ts new file mode 100644 index 000000000..919c8f019 --- /dev/null +++ b/packages/blob/src/utils/WebBlob.spec.ts @@ -0,0 +1,83 @@ +import { describe, expect, it, beforeAll } from "vitest"; +import { WebBlob } from "./WebBlob"; + +describe("WebBlob", () => { + const resourceUrl = new URL("https://huggingface.co/spaces/aschen/push-model-from-web/raw/main/mobilenet/model.json"); + let fullText: string; + let size: number; + let contentType: string; + + beforeAll(async () => { + const response = await fetch(resourceUrl, { method: "HEAD" }); + size = Number(response.headers.get("content-length")); + contentType = response.headers.get("content-type") || ""; + fullText = await (await fetch(resourceUrl)).text(); + }); + + it("should create a WebBlob with a slice on the entire resource", async () => { + const webBlob = await WebBlob.create(resourceUrl, { cacheBelow: 0 }); + + expect(webBlob).toMatchObject({ + url: resourceUrl, + start: 0, + end: size, + contentType, + }); + expect(webBlob).toBeInstanceOf(WebBlob); + expect(webBlob.size).toBe(size); + expect(webBlob.type).toBe(contentType); + + const text = await webBlob.text(); + expect(text).toBe(fullText); + + const streamText = await new Response(webBlob.stream()).text(); + expect(streamText).toBe(fullText); + }); + + it("should create a WebBlob with a slice on the entire resource, cached", async () => { + const webBlob = await WebBlob.create(resourceUrl, { cacheBelow: 1_000_000 }); + + expect(webBlob).not.toBeInstanceOf(WebBlob); + expect(webBlob.size).toBe(size); + expect(webBlob.type.replace(/;\s*charset=utf-8/, "")).toBe(contentType.replace(/;\s*charset=utf-8/, "")); + + const text = await webBlob.text(); + expect(text).toBe(fullText); + + const streamText = await new Response(webBlob.stream()).text(); + expect(streamText).toBe(fullText); + }); + + it("should lazy load a LFS file hosted on Hugging Face", async () => { + const stableDiffusionUrl = + "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/unet/diffusion_pytorch_model.fp16.safetensors"; + const url = new URL(stableDiffusionUrl); + const webBlob = await WebBlob.create(url); + + expect(webBlob.size).toBe(5_135_149_760); + expect(webBlob).toBeInstanceOf(WebBlob); + expect(webBlob).toMatchObject({ url }); + expect(await webBlob.slice(10, 22).text()).toBe("__metadata__"); + }); + + it("should create a slice on the file", async () => { + const expectedText = fullText.slice(10, 20); + + const slice = (await WebBlob.create(resourceUrl, { cacheBelow: 0 })).slice(10, 20); + + expect(slice).toMatchObject({ + url: resourceUrl, + start: 10, + end: 20, + contentType, + }); + expect(slice.size).toBe(10); + expect(slice.type).toBe(contentType); + + const sliceText = await slice.text(); + expect(sliceText).toBe(expectedText); + + const streamText = await new Response(slice.stream()).text(); + expect(streamText).toBe(expectedText); + }); +}); diff --git a/packages/blob/src/utils/WebBlob.ts b/packages/blob/src/utils/WebBlob.ts new file mode 100644 index 000000000..fe35813fe --- /dev/null +++ b/packages/blob/src/utils/WebBlob.ts @@ -0,0 +1,111 @@ +/** + * WebBlob is a Blob implementation for web resources that supports range requests. + */ + +interface WebBlobCreateOptions { + /** + * @default 1_000_000 + * + * Objects below that size will immediately be fetched and put in RAM, rather + * than streamed ad-hoc + */ + cacheBelow?: number; + /** + * Custom fetch function to use instead of the default one, for example to use a proxy or edit headers. + */ + fetch?: typeof fetch; +} + +export class WebBlob extends Blob { + static async create(url: URL, opts?: WebBlobCreateOptions): Promise { + const customFetch = opts?.fetch ?? fetch; + const response = await customFetch(url, { method: "HEAD" }); + + const size = Number(response.headers.get("content-length")); + const contentType = response.headers.get("content-type") || ""; + const supportRange = response.headers.get("accept-ranges") === "bytes"; + + if (!supportRange || size < (opts?.cacheBelow ?? 1_000_000)) { + return await (await customFetch(url)).blob(); + } + + return new WebBlob(url, 0, size, contentType, true, customFetch); + } + + private url: URL; + private start: number; + private end: number; + private contentType: string; + private full: boolean; + private fetch: typeof fetch; + + constructor(url: URL, start: number, end: number, contentType: string, full: boolean, customFetch: typeof fetch) { + super([]); + + this.url = url; + this.start = start; + this.end = end; + this.contentType = contentType; + this.full = full; + this.fetch = customFetch; + } + + override get size(): number { + return this.end - this.start; + } + + override get type(): string { + return this.contentType; + } + + override slice(start = 0, end = this.size): WebBlob { + if (start < 0 || end < 0) { + new TypeError("Unsupported negative start/end on FileBlob.slice"); + } + + const slice = new WebBlob( + this.url, + this.start + start, + Math.min(this.start + end, this.end), + this.contentType, + start === 0 && end === this.size ? this.full : false, + this.fetch + ); + + return slice; + } + + override async arrayBuffer(): Promise { + const result = await this.fetchRange(); + + return result.arrayBuffer(); + } + + override async text(): Promise { + const result = await this.fetchRange(); + + return result.text(); + } + + override stream(): ReturnType { + const stream = new TransformStream(); + + this.fetchRange() + .then((response) => response.body?.pipeThrough(stream)) + .catch((error) => stream.writable.abort(error.message)); + + return stream.readable; + } + + private fetchRange(): Promise { + const fetch = this.fetch; // to avoid this.fetch() which is bound to the instance instead of globalThis + if (this.full) { + return fetch(this.url); + } + return fetch(this.url, { + headers: { + Range: `bytes=${this.start}-${this.end - 1}`, + }, + }); + } +} diff --git a/packages/blob/src/utils/createBlob.ts b/packages/blob/src/utils/createBlob.ts new file mode 100644 index 000000000..0cf54206d --- /dev/null +++ b/packages/blob/src/utils/createBlob.ts @@ -0,0 +1,30 @@ +import { WebBlob } from "./WebBlob"; +import { isFrontend } from "./isFrontend"; + +/** + * This function allow to retrieve either a FileBlob or a WebBlob from a URL. + * + * From the backend: + * - support local files + * - support http resources with absolute URLs + * + * From the frontend: + * - support http resources with absolute or relative URLs + */ +export async function createBlob(url: URL, opts?: { fetch?: typeof fetch }): Promise { + if (url.protocol === "http:" || url.protocol === "https:") { + return WebBlob.create(url, { fetch: opts?.fetch }); + } + + if (isFrontend) { + throw new TypeError(`Unsupported URL protocol "${url.protocol}"`); + } + + if (url.protocol === "file:") { + const { FileBlob } = await import("./FileBlob"); + + return FileBlob.create(url); + } + + throw new TypeError(`Unsupported URL protocol "${url.protocol}"`); +} diff --git a/packages/blob/src/utils/isBackend.ts b/packages/blob/src/utils/isBackend.ts new file mode 100644 index 000000000..1e6f27998 --- /dev/null +++ b/packages/blob/src/utils/isBackend.ts @@ -0,0 +1,6 @@ +const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"; + +const isWebWorker = + typeof self === "object" && self.constructor && self.constructor.name === "DedicatedWorkerGlobalScope"; + +export const isBackend = !isBrowser && !isWebWorker; diff --git a/packages/blob/src/utils/isFrontend.ts b/packages/blob/src/utils/isFrontend.ts new file mode 100644 index 000000000..0b9bab392 --- /dev/null +++ b/packages/blob/src/utils/isFrontend.ts @@ -0,0 +1,3 @@ +import { isBackend } from "./isBackend"; + +export const isFrontend = !isBackend; diff --git a/packages/blob/tsconfig.json b/packages/blob/tsconfig.json new file mode 100644 index 000000000..254606a30 --- /dev/null +++ b/packages/blob/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "lib": ["ES2022", "DOM"], + "module": "CommonJS", + "moduleResolution": "node", + "target": "ES2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "skipLibCheck": true, + "noImplicitOverride": true, + "outDir": "./dist", + "declaration": true, + "declarationMap": true + }, + "include": ["src", "index.ts"], + "exclude": ["dist"] +} diff --git a/packages/blob/tsup.config.ts b/packages/blob/tsup.config.ts new file mode 100644 index 000000000..c08bf2a12 --- /dev/null +++ b/packages/blob/tsup.config.ts @@ -0,0 +1,25 @@ +import type { Options } from "tsup"; + +const baseConfig: Options = { + entry: ["./index.ts"], + format: ["cjs", "esm"], + outDir: "dist", + clean: true, +}; + +const nodeConfig: Options = { + ...baseConfig, + entry: ["./index.ts", "./src/utils/WebBlob.ts", "./src/utils/FileBlob.ts"], + platform: "node", +}; + +const browserConfig: Options = { + ...baseConfig, + entry: ["./index.ts", "./src/utils/WebBlob.ts"], + platform: "browser", + target: "es2018", + splitting: true, + outDir: "dist/browser", +}; + +export default [nodeConfig, browserConfig]; diff --git a/packages/blob/vitest-browser.config.mts b/packages/blob/vitest-browser.config.mts new file mode 100644 index 000000000..65be77c7a --- /dev/null +++ b/packages/blob/vitest-browser.config.mts @@ -0,0 +1,7 @@ +import { configDefaults, defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + exclude: [...configDefaults.exclude, "src/utils/FileBlob.spec.ts"], + }, +}); diff --git a/packages/dduf/.eslintignore b/packages/dduf/.eslintignore new file mode 100644 index 000000000..67c4f0d9d --- /dev/null +++ b/packages/dduf/.eslintignore @@ -0,0 +1,2 @@ +dist +sha256.js diff --git a/packages/dduf/.prettierignore b/packages/dduf/.prettierignore new file mode 100644 index 000000000..cac0c6949 --- /dev/null +++ b/packages/dduf/.prettierignore @@ -0,0 +1,4 @@ +pnpm-lock.yaml +# In order to avoid code samples to have tabs, they don't display well on npm +README.md +dist \ No newline at end of file diff --git a/packages/dduf/LICENSE b/packages/dduf/LICENSE new file mode 100644 index 000000000..5e9693e35 --- /dev/null +++ b/packages/dduf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Hugging Face + +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/packages/dduf/README.md b/packages/dduf/README.md new file mode 100644 index 000000000..993333965 --- /dev/null +++ b/packages/dduf/README.md @@ -0,0 +1,33 @@ +# 🤗 Hugging Face DDUF + +Very alpha version of a DDUF checker / parser. + +## Install + +```console +pnpm add @huggingface/dduf + +npm add @huggingface/dduf + +yarn add @huggingface/dduf +``` + +### Deno + +```ts +// esm.sh +import { checkDDUF } from "https://esm.sh/@huggingface/dduf"; +// or npm: +import { checkDDUF } from "npm:@huggingface/dduf"; +``` + +## Usage + + +```ts +import { checkDDUF } from "@huggingface/dduf"; + +for await (const entry of checkDDUF(URL | Blob, { log: console.log })) { + console.log("file", entry); +} +``` diff --git a/packages/dduf/index.ts b/packages/dduf/index.ts new file mode 100644 index 000000000..3bd16e178 --- /dev/null +++ b/packages/dduf/index.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/packages/dduf/package.json b/packages/dduf/package.json new file mode 100644 index 000000000..56e1db55c --- /dev/null +++ b/packages/dduf/package.json @@ -0,0 +1,58 @@ +{ + "name": "@huggingface/dduf", + "packageManager": "pnpm@8.10.5", + "version": "0.0.2", + "description": "Very alpha lib to check DDUF compliance", + "repository": "https://github.com/huggingface/huggingface.js.git", + "publishConfig": { + "access": "public" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.mjs" + }, + "./package.json": "./package.json" + }, + "browser": { + "./dist/index.js": "./dist/browser/index.js", + "./dist/index.mjs": "./dist/browser/index.mjs" + }, + "source": "index.ts", + "scripts": { + "lint": "eslint --quiet --fix --ext .cjs,.ts .", + "lint:check": "eslint --ext .cjs,.ts .", + "format": "prettier --write .", + "format:check": "prettier --check .", + "prepublishOnly": "pnpm run build", + "build": "tsup && tsc --emitDeclarationOnly --declaration", + "prepare": "pnpm run build", + "test": "vitest run", + "test:browser": "vitest run --browser.name=chrome --browser.headless", + "check": "tsc" + }, + "files": [ + "src", + "dist", + "index.ts", + "tsconfig.json" + ], + "keywords": [ + "huggingface", + "hugging", + "face", + "dduf" + ], + "author": "Hugging Face", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.11.28" + }, + "dependencies": { + "@huggingface/blob": "workspace:^" + } +} diff --git a/packages/dduf/pnpm-lock.yaml b/packages/dduf/pnpm-lock.yaml new file mode 100644 index 000000000..0c7f7196a --- /dev/null +++ b/packages/dduf/pnpm-lock.yaml @@ -0,0 +1,27 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@huggingface/blob': + specifier: workspace:^ + version: link:../blob + +devDependencies: + '@types/node': + specifier: ^20.11.28 + version: 20.17.9 + +packages: + + /@types/node@20.17.9: + resolution: {integrity: sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==} + dependencies: + undici-types: 6.19.8 + dev: true + + /undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + dev: true diff --git a/packages/dduf/src/check-dduf.spec.ts b/packages/dduf/src/check-dduf.spec.ts new file mode 100644 index 000000000..ad98b9823 --- /dev/null +++ b/packages/dduf/src/check-dduf.spec.ts @@ -0,0 +1,178 @@ +import { describe, expect, it } from "vitest"; +import { checkDDUF, type DDUFFileEntry } from "./check-dduf"; + +describe("check-dduf", () => { + it("should work", async () => { + const files: DDUFFileEntry[] = []; + for await (const file of checkDDUF( + new URL("https://huggingface.co/spaces/coyotte508/dduf-check/resolve/main/file-64.dduf") + )) { + files.push(file); + } + + expect(files).toEqual([ + { + fileHeaderOffset: 0, + name: "vae/", + size: 0, + type: "file", + }, + { + fileHeaderOffset: 82, + name: "vae/config.json", + size: 3, + type: "file", + }, + { + fileHeaderOffset: 178, + name: "vae/diffusion_pytorch_model.safetensors", + size: 0, + type: "file", + }, + { + fileHeaderOffset: 295, + name: "text_encoder_2/", + size: 0, + type: "file", + }, + { + fileHeaderOffset: 388, + name: "text_encoder_2/config.json", + size: 3, + type: "file", + }, + { + fileHeaderOffset: 495, + name: "text_encoder_2/model-00002-of-00002.safetensors", + size: 0, + type: "file", + }, + { + fileHeaderOffset: 620, + name: "text_encoder_2/models.saftensors.index.json", + size: 3, + type: "file", + }, + { + fileHeaderOffset: 744, + name: "text_encoder_2/model-00001-of-00002.safetensors", + size: 0, + type: "file", + }, + { + fileHeaderOffset: 869, + name: "transformer/", + size: 0, + type: "file", + }, + { + fileHeaderOffset: 959, + name: "transformer/config.json", + size: 3, + type: "file", + }, + { + fileHeaderOffset: 1063, + name: "transformer/diffusion_pytorch_model.safetensors", + size: 0, + type: "file", + }, + { + fileHeaderOffset: 1188, + name: "tokenizer_2/", + size: 0, + type: "file", + }, + { + fileHeaderOffset: 1278, + name: "tokenizer_2/vocab.json", + size: 3, + type: "file", + }, + { + fileHeaderOffset: 1381, + name: "tokenizer_2/special_tokens_map.json", + size: 3, + type: "file", + }, + { + fileHeaderOffset: 1497, + name: "tokenizer_2/tokenizer_config.json", + size: 3, + type: "file", + }, + { + fileHeaderOffset: 1611, + name: "tokenizer_2/spiece.gguf", + size: 0, + type: "file", + }, + { + fileHeaderOffset: 1712, + name: "tokenizer/", + size: 0, + type: "file", + }, + { + fileHeaderOffset: 1800, + name: "tokenizer/vocab.json", + size: 3, + type: "file", + }, + { + fileHeaderOffset: 1901, + name: "tokenizer/special_tokens_map.json", + size: 3, + type: "file", + }, + { + fileHeaderOffset: 2015, + name: "tokenizer/tokenizer_config.json", + size: 3, + type: "file", + }, + { + fileHeaderOffset: 2127, + name: "scheduler/", + size: 0, + type: "file", + }, + { + fileHeaderOffset: 2215, + name: "scheduler/scheduler-config.json", + size: 3, + type: "file", + }, + { + fileHeaderOffset: 2327, + name: "text_encoder/", + size: 0, + type: "file", + }, + { + fileHeaderOffset: 2418, + name: "text_encoder/config.json", + size: 3, + type: "file", + }, + { + fileHeaderOffset: 2523, + name: "text_encoder/model-00002-of-00002.safetensors", + size: 0, + type: "file", + }, + { + fileHeaderOffset: 2646, + name: "text_encoder/models.saftensors.index.json", + size: 3, + type: "file", + }, + { + fileHeaderOffset: 2768, + name: "text_encoder/model-00001-of-00002.safetensors", + size: 0, + type: "file", + }, + ]); + }); +}); diff --git a/packages/dduf/src/check-dduf.ts b/packages/dduf/src/check-dduf.ts new file mode 100644 index 000000000..a4f17a959 --- /dev/null +++ b/packages/dduf/src/check-dduf.ts @@ -0,0 +1,216 @@ +import { checkFilename } from "./check-filename"; +import { createBlob } from "@huggingface/blob"; + +export interface DDUFFileEntry { + type: "file"; + name: string; + size: number; + fileHeaderOffset: number; +} + +export async function* checkDDUF(url: Blob | URL, opts?: { log?: (x: string) => void }): AsyncGenerator { + const blob = url instanceof Blob ? url : await createBlob(url); + + opts?.log?.("File size: " + blob.size); + + // DDUF is a zip file, uncompressed. + + const last100kB = await blob.slice(blob.size - 100_000, blob.size).arrayBuffer(); + + const view = new DataView(last100kB); + + let index = view.byteLength - 22; + let found = false; + + while (index >= 0) { + if (view.getUint32(index, true) === 0x06054b50) { + found = true; + break; + } + + index--; + } + + if (!found) { + throw new Error("DDUF footer not found in last 100kB of file"); + } + + opts?.log?.("DDUF footer found at offset " + (blob.size - last100kB.byteLength + index)); + + const diskNumber = view.getUint16(index + 4, true); + + opts?.log?.("Disk number: " + diskNumber); + + if (diskNumber !== 0 && diskNumber !== 0xffff) { + throw new Error("Multi-disk archives not supported"); + } + + let fileCount = view.getUint16(index + 10, true); + let centralDirSize = view.getUint32(index + 12, true); + let centralDirOffset = view.getUint32(index + 16, true); + const isZip64 = centralDirOffset === 0xffffffff; + + opts?.log?.("File count: " + fileCount); + + if (isZip64) { + opts?.log?.("Zip64 format detected"); + + index -= 20; + found = false; + while (index >= 0) { + if (view.getUint32(index, true) === 0x07064b50) { + found = true; + break; + } + + index--; + } + + if (!found) { + throw new Error("Zip64 footer not found in last 100kB of file"); + } + + opts?.log?.("Zip64 footer found at offset " + (blob.size - last100kB.byteLength + index)); + + const diskWithCentralDir = view.getUint32(index + 4, true); + + if (diskWithCentralDir !== 0) { + throw new Error("Multi-disk archives not supported"); + } + + const endCentralDirOffset = Number(view.getBigUint64(index + 8, true)); + + index = endCentralDirOffset - (blob.size - last100kB.byteLength); + + if (index < 0) { + throw new Error("Central directory offset is outside the last 100kB of the file"); + } + + if (view.getUint32(index, true) !== 0x06064b50) { + throw new Error("Invalid central directory header"); + } + + const thisDisk = view.getUint16(index + 16, true); + const centralDirDisk = view.getUint16(index + 20, true); + + if (thisDisk !== 0) { + throw new Error("Multi-disk archives not supported"); + } + + if (centralDirDisk !== 0) { + throw new Error("Multi-disk archives not supported"); + } + + centralDirSize = Number(view.getBigUint64(index + 40, true)); + centralDirOffset = Number(view.getBigUint64(index + 48, true)); + fileCount = Number(view.getBigUint64(index + 32, true)); + + opts?.log?.("File count zip 64: " + fileCount); + } + + opts?.log?.("Central directory size: " + centralDirSize); + opts?.log?.("Central directory offset: " + centralDirOffset); + + const centralDir = + centralDirOffset > blob.size - last100kB.byteLength + ? last100kB.slice( + centralDirOffset - (blob.size - last100kB.byteLength), + centralDirOffset - (blob.size - last100kB.byteLength) + centralDirSize + ) + : await blob.slice(centralDirOffset, centralDirOffset + centralDirSize).arrayBuffer(); + + const centralDirView = new DataView(centralDir); + let offset = 0; + + for (let i = 0; i < fileCount; i++) { + if (centralDirView.getUint32(offset + 0, true) !== 0x02014b50) { + throw new Error("Invalid central directory file header"); + } + + if (offset + 46 > centralDir.byteLength) { + throw new Error("Unexpected end of central directory"); + } + + const compressionMethod = centralDirView.getUint16(offset + 10, true); + + if (compressionMethod !== 0) { + throw new Error("Unsupported compression method: " + compressionMethod); + } + + const filenameLength = centralDirView.getUint16(offset + 28, true); + const fileName = new TextDecoder().decode(new Uint8Array(centralDir, offset + 46, filenameLength)); + + opts?.log?.("File " + i); + opts?.log?.("File name: " + fileName); + + checkFilename(fileName); + + const fileDiskNumber = centralDirView.getUint16(34, true); + + if (fileDiskNumber !== 0 && fileDiskNumber !== 0xffff) { + throw new Error("Multi-disk archives not supported"); + } + + let size = centralDirView.getUint32(offset + 24, true); + let compressedSize = centralDirView.getUint32(offset + 20, true); + let filePosition = centralDirView.getUint32(offset + 42, true); + + const extraFieldLength = centralDirView.getUint16(offset + 30, true); + + if (size === 0xffffffff || compressedSize === 0xffffffff || filePosition === 0xffffffff) { + opts?.log?.("File size is in zip64 format"); + + const extraFields = new DataView(centralDir, offset + 46 + filenameLength, extraFieldLength); + + let extraFieldOffset = 0; + + while (extraFieldOffset < extraFieldLength) { + const headerId = extraFields.getUint16(extraFieldOffset, true); + const extraFieldSize = extraFields.getUint16(extraFieldOffset + 2, true); + if (headerId !== 0x0001) { + extraFieldOffset += 4 + extraFieldSize; + continue; + } + + const zip64ExtraField = new DataView( + centralDir, + offset + 46 + filenameLength + extraFieldOffset + 4, + extraFieldSize + ); + let zip64ExtraFieldOffset = 0; + + if (size === 0xffffffff) { + size = Number(zip64ExtraField.getBigUint64(zip64ExtraFieldOffset, true)); + zip64ExtraFieldOffset += 8; + } + + if (compressedSize === 0xffffffff) { + compressedSize = Number(zip64ExtraField.getBigUint64(zip64ExtraFieldOffset, true)); + zip64ExtraFieldOffset += 8; + } + + if (filePosition === 0xffffffff) { + filePosition = Number(zip64ExtraField.getBigUint64(zip64ExtraFieldOffset, true)); + zip64ExtraFieldOffset += 8; + } + + break; + } + } + + if (size !== compressedSize) { + throw new Error("Compressed size and size differ: " + compressedSize + " vs " + size); + } + opts?.log?.("File size: " + size); + + const commentLength = centralDirView.getUint16(offset + 32, true); + + opts?.log?.("File header position in archive: " + filePosition); + + offset += 46 + filenameLength + extraFieldLength + commentLength; + + yield { type: "file", name: fileName, size, fileHeaderOffset: filePosition }; + } + + opts?.log?.("All files checked"); +} diff --git a/packages/dduf/src/check-filename.ts b/packages/dduf/src/check-filename.ts new file mode 100644 index 000000000..5eba2548c --- /dev/null +++ b/packages/dduf/src/check-filename.ts @@ -0,0 +1,17 @@ +export function checkFilename(filename: string): void { + if ( + !filename.endsWith(".safetensors") && + !filename.endsWith(".json") && + !filename.endsWith(".gguf") && + !filename.endsWith(".txt") && + !filename.endsWith("/") + ) { + throw new Error("Files must have a .safetensors, .txt, .gguf or .json extension"); + } + + const split = filename.split("/"); + + if (split.length > 2) { + throw new Error("Files must be only one level deep, not more"); + } +} diff --git a/packages/dduf/src/index.ts b/packages/dduf/src/index.ts new file mode 100644 index 000000000..ab8badb8e --- /dev/null +++ b/packages/dduf/src/index.ts @@ -0,0 +1 @@ +export * from "./check-dduf"; diff --git a/packages/dduf/tsconfig.json b/packages/dduf/tsconfig.json new file mode 100644 index 000000000..254606a30 --- /dev/null +++ b/packages/dduf/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "lib": ["ES2022", "DOM"], + "module": "CommonJS", + "moduleResolution": "node", + "target": "ES2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "skipLibCheck": true, + "noImplicitOverride": true, + "outDir": "./dist", + "declaration": true, + "declarationMap": true + }, + "include": ["src", "index.ts"], + "exclude": ["dist"] +} diff --git a/packages/dduf/tsup.config.ts b/packages/dduf/tsup.config.ts new file mode 100644 index 000000000..8306dcbfc --- /dev/null +++ b/packages/dduf/tsup.config.ts @@ -0,0 +1,25 @@ +import type { Options } from "tsup"; + +const baseConfig: Options = { + entry: ["./index.ts"], + format: ["cjs", "esm"], + outDir: "dist", + clean: true, +}; + +const nodeConfig: Options = { + ...baseConfig, + entry: ["./index.ts"], + platform: "node", +}; + +const browserConfig: Options = { + ...baseConfig, + entry: ["./index.ts"], + platform: "browser", + target: "es2018", + splitting: true, + outDir: "dist/browser", +}; + +export default [nodeConfig, browserConfig]; diff --git a/packages/gguf/package.json b/packages/gguf/package.json index 53ae965b2..af13d7096 100644 --- a/packages/gguf/package.json +++ b/packages/gguf/package.json @@ -27,7 +27,6 @@ }, "source": "index.ts", "scripts": { - "prepare": "pnpm run build", "lint": "eslint --quiet --fix --ext .cjs,.ts .", "lint:check": "eslint --ext .cjs,.ts .", "format": "prettier --write .", @@ -50,6 +49,9 @@ ], "author": "Hugging Face", "license": "MIT", + "dependencies": { + "@huggingface/tasks": "workspace:^" + }, "devDependencies": { "@types/node": "^20.12.8" } diff --git a/packages/gguf/pnpm-lock.yaml b/packages/gguf/pnpm-lock.yaml index ebb2107de..c96b1898a 100644 --- a/packages/gguf/pnpm-lock.yaml +++ b/packages/gguf/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +dependencies: + '@huggingface/tasks': + specifier: workspace:^ + version: link:../tasks + devDependencies: '@types/node': specifier: ^20.12.8 diff --git a/packages/gguf/src/gguf.spec.ts b/packages/gguf/src/gguf.spec.ts index 5e1d08226..58852dbee 100644 --- a/packages/gguf/src/gguf.spec.ts +++ b/packages/gguf/src/gguf.spec.ts @@ -33,7 +33,7 @@ describe("gguf", () => { const arrayBuf = await res.arrayBuffer(); fs.writeFileSync(".cache/model.gguf", Buffer.from(arrayBuf)); } - }); + }, 30_000); it("should parse a llama2 7b", async () => { const { metadata, tensorInfos } = await gguf(URL_LLAMA); diff --git a/packages/gguf/src/gguf.ts b/packages/gguf/src/gguf.ts index 0efd43aaa..d83b405da 100644 --- a/packages/gguf/src/gguf.ts +++ b/packages/gguf/src/gguf.ts @@ -1,11 +1,12 @@ import type { MetadataValue, Version, GGUFMetadata, GGUFTensorInfo, GGUFParseOutput } from "./types"; -import { GGMLQuantizationType, GGUFValueType } from "./types"; +import { GGUFValueType } from "./types"; import { isBackend } from "./utils/isBackend"; import { promisesQueue } from "./utils/promisesQueue"; export type { MetadataBaseValue, MetadataValue, Version, GGUFMetadata, GGUFTensorInfo, GGUFParseOutput } from "./types"; export { GGUFValueType, GGMLFileQuantizationType, GGMLQuantizationType, Architecture } from "./types"; export { GGUF_QUANT_DESCRIPTIONS } from "./quant-descriptions"; +export { parseGGUFQuantLabel, GGUF_QUANT_RE, GGUF_QUANT_RE_GLOBAL } from "@huggingface/tasks"; export const RE_GGUF_FILE = /\.gguf$/; export const RE_GGUF_SHARD_FILE = /^(?.*?)-(?\d{5})-of-(?\d{5})\.gguf$/; @@ -29,15 +30,6 @@ export function parseGgufShardFilename(filename: string): GgufShardFileInfo | nu return null; } -const ggufQuants = Object.values(GGMLQuantizationType).filter((v): v is string => typeof v === "string"); -export const GGUF_QUANT_RE = new RegExp(`(?${ggufQuants.join("|")})` + "(_(?[A-Z]+))?"); -export const GGUF_QUANT_RE_GLOBAL = new RegExp(GGUF_QUANT_RE, "g"); - -export function parseGGUFQuantLabel(fname: string): string | undefined { - const quantLabel = fname.toUpperCase().match(GGUF_QUANT_RE_GLOBAL)?.at(-1); // if there is multiple quant substrings in a name, we prefer the last one - return quantLabel; -} - const isVersion = (version: number): version is Version => version === 1 || version === 2 || version === 3; /** diff --git a/packages/gguf/src/types.ts b/packages/gguf/src/types.ts index 02872b95c..4a6b40e16 100644 --- a/packages/gguf/src/types.ts +++ b/packages/gguf/src/types.ts @@ -1,5 +1,7 @@ import type { TransformerLLM } from "./transformer-llm"; import { LLM_ARCHITECTURES } from "./transformer-llm"; +import type { GGMLQuantizationType } from "@huggingface/tasks"; +export { GGMLQuantizationType } from "@huggingface/tasks"; export type MetadataBaseValue = string | number | bigint | boolean; export type MetadataValue = MetadataBaseValue | MetadataBaseValue[] | MetadataValue[]; /// recursive as arrays can be nested. @@ -45,38 +47,6 @@ export enum GGMLFileQuantizationType { MOSTLY_Q4_0_8_8 = 35, } -export enum GGMLQuantizationType { - F32 = 0, - F16 = 1, - Q4_0 = 2, - Q4_1 = 3, - Q5_0 = 6, - Q5_1 = 7, - Q8_0 = 8, - Q8_1 = 9, - Q2_K = 10, - Q3_K = 11, - Q4_K = 12, - Q5_K = 13, - Q6_K = 14, - Q8_K = 15, - IQ2_XXS = 16, - IQ2_XS = 17, - IQ3_XXS = 18, - IQ1_S = 19, - IQ4_NL = 20, - IQ3_S = 21, - IQ2_S = 22, - IQ4_XS = 23, - I8 = 24, - I16 = 25, - I32 = 26, - I64 = 27, - F64 = 28, - IQ1_M = 29, - BF16 = 30, -} - export enum GGUFValueType { UINT8 = 0, INT8 = 1, diff --git a/packages/hub/README.md b/packages/hub/README.md index c9a650a5c..3d87c15dc 100644 --- a/packages/hub/README.md +++ b/packages/hub/README.md @@ -117,6 +117,10 @@ Checkout the demo: https://huggingface.co/spaces/huggingfacejs/client-side-oauth The `@huggingface/hub` package provide basic capabilities to scan the cache directory. Learn more about [Manage huggingface_hub cache-system](https://huggingface.co/docs/huggingface_hub/en/guides/manage-cache). +### `scanCacheDir` + +You can get the list of cached repositories using the `scanCacheDir` function. + ```ts import { scanCacheDir } from "@huggingface/hub"; @@ -124,7 +128,40 @@ const result = await scanCacheDir(); console.log(result); ``` -Note that the cache directory is created and used only by the Python and Rust libraries. Downloading files using the `@huggingface/hub` package won't use the cache directory. +Note: this does not work in the browser + +### `downloadFileToCacheDir` + +You can cache a file of a repository using the `downloadFileToCacheDir` function. + +```ts +import { downloadFileToCacheDir } from "@huggingface/hub"; + +const file = await downloadFileToCacheDir({ + repo: 'foo/bar', + path: 'README.md' +}); + +console.log(file); +``` +Note: this does not work in the browser + +### `snapshotDownload` + +You can download an entire repository at a given revision in the cache directory using the `snapshotDownload` function. + +```ts +import { snapshotDownload } from "@huggingface/hub"; + +const directory = await snapshotDownload({ + repo: 'foo/bar', +}); + +console.log(directory); +``` +The code use internally the `downloadFileToCacheDir` function. + +Note: this does not work in the browser ## Performance considerations diff --git a/packages/hub/package.json b/packages/hub/package.json index 8ad93a2b5..c9a21f096 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -1,7 +1,7 @@ { "name": "@huggingface/hub", "packageManager": "pnpm@8.10.5", - "version": "0.18.2", + "version": "1.0.0", "description": "Utilities to interact with the Hugging Face hub", "repository": "https://github.com/huggingface/huggingface.js.git", "publishConfig": { @@ -21,6 +21,8 @@ "./src/utils/sha256-node.ts": false, "./src/utils/FileBlob.ts": false, "./src/lib/cache-management.ts": false, + "./src/lib/download-file-to-cache-dir.ts": false, + "./src/lib/snapshot-download.ts": false, "./dist/index.js": "./dist/browser/index.js", "./dist/index.mjs": "./dist/browser/index.mjs" }, diff --git a/packages/hub/src/error.ts b/packages/hub/src/error.ts index f0a3e33c4..0da5b2dd9 100644 --- a/packages/hub/src/error.ts +++ b/packages/hub/src/error.ts @@ -15,6 +15,9 @@ export async function createApiError( if (response.headers.get("Content-Type")?.startsWith("application/json")) { const json = await response.json(); error.message = json.error || json.message || error.message; + if (json.error_description) { + error.message = error.message ? error.message + `: ${json.error_description}` : json.error_description; + } error.data = json; } else { error.data = { message: await response.text() }; diff --git a/packages/hub/src/lib/cache-management.ts b/packages/hub/src/lib/cache-management.ts index aecbf271e..84b664077 100644 --- a/packages/hub/src/lib/cache-management.ts +++ b/packages/hub/src/lib/cache-management.ts @@ -16,12 +16,19 @@ function getHuggingFaceHubCache(): string { return process.env["HUGGINGFACE_HUB_CACHE"] ?? getDefaultCachePath(); } -function getHFHubCache(): string { +export function getHFHubCachePath(): string { return process.env["HF_HUB_CACHE"] ?? getHuggingFaceHubCache(); } const FILES_TO_IGNORE: string[] = [".DS_Store"]; +export const REPO_ID_SEPARATOR: string = "--"; + +export function getRepoFolderName({ name, type }: RepoId): string { + const parts = [`${type}s`, ...name.split("/")]; + return parts.join(REPO_ID_SEPARATOR); +} + export interface CachedFileInfo { path: string; /** @@ -63,7 +70,7 @@ export interface HFCacheInfo { } export async function scanCacheDir(cacheDir: string | undefined = undefined): Promise { - if (!cacheDir) cacheDir = getHFHubCache(); + if (!cacheDir) cacheDir = getHFHubCachePath(); const s = await stat(cacheDir); if (!s.isDirectory()) { @@ -107,12 +114,12 @@ export async function scanCacheDir(cacheDir: string | undefined = undefined): Pr export async function scanCachedRepo(repoPath: string): Promise { // get the directory name const name = basename(repoPath); - if (!name.includes("--")) { + if (!name.includes(REPO_ID_SEPARATOR)) { throw new Error(`Repo path is not a valid HuggingFace cache directory: ${name}`); } // parse the repoId from directory name - const [type, ...remaining] = name.split("--"); + const [type, ...remaining] = name.split(REPO_ID_SEPARATOR); const repoType = parseRepoType(type); const repoId = remaining.join("/"); diff --git a/packages/hub/src/lib/create-repo.ts b/packages/hub/src/lib/create-repo.ts index 4005844d6..c0323dc11 100644 --- a/packages/hub/src/lib/create-repo.ts +++ b/packages/hub/src/lib/create-repo.ts @@ -9,6 +9,9 @@ import { toRepoId } from "../utils/toRepoId"; export async function createRepo( params: { repo: RepoDesignation; + /** + * If unset, will follow the organization's default setting. (typically public, except for some Enterprise organizations) + */ private?: boolean; license?: string; /** diff --git a/packages/hub/src/lib/dataset-info.spec.ts b/packages/hub/src/lib/dataset-info.spec.ts index 8f944de6d..ae235e5e8 100644 --- a/packages/hub/src/lib/dataset-info.spec.ts +++ b/packages/hub/src/lib/dataset-info.spec.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; import { datasetInfo } from "./dataset-info"; +import type { DatasetEntry } from "./list-datasets"; +import type { ApiDatasetInfo } from "../types/api/api-dataset"; describe("datasetInfo", () => { it("should return the dataset info", async () => { @@ -16,4 +18,39 @@ describe("datasetInfo", () => { private: false, }); }); + + it("should return the dataset info with author", async () => { + const info: DatasetEntry & Pick = await datasetInfo({ + name: "nyu-mll/glue", + additionalFields: ["author"], + }); + expect(info).toEqual({ + id: "621ffdd236468d709f181e3f", + downloads: expect.any(Number), + gated: false, + name: "nyu-mll/glue", + updatedAt: expect.any(Date), + likes: expect.any(Number), + private: false, + author: "nyu-mll", + }); + }); + + it("should return the dataset info for a specific revision", async () => { + const info: DatasetEntry & Pick = await datasetInfo({ + name: "nyu-mll/glue", + revision: "cb2099c76426ff97da7aa591cbd317d91fb5fcb7", + additionalFields: ["sha"], + }); + expect(info).toEqual({ + id: "621ffdd236468d709f181e3f", + downloads: expect.any(Number), + gated: false, + name: "nyu-mll/glue", + updatedAt: expect.any(Date), + likes: expect.any(Number), + private: false, + sha: "cb2099c76426ff97da7aa591cbd317d91fb5fcb7", + }); + }); }); diff --git a/packages/hub/src/lib/dataset-info.ts b/packages/hub/src/lib/dataset-info.ts index 42f479d04..542b5aa0f 100644 --- a/packages/hub/src/lib/dataset-info.ts +++ b/packages/hub/src/lib/dataset-info.ts @@ -13,6 +13,10 @@ export async function datasetInfo< name: string; hubUrl?: string; additionalFields?: T[]; + /** + * An optional Git revision id which can be a branch name, a tag, or a commit hash. + */ + revision?: string; /** * Custom fetch function to use instead of the default one, for example to use a proxy or edit headers. */ @@ -27,7 +31,9 @@ export async function datasetInfo< ]).toString(); const response = await (params.fetch || fetch)( - `${params?.hubUrl || HUB_URL}/api/datasets/${params.name}?${search.toString()}`, + `${params?.hubUrl || HUB_URL}/api/datasets/${params.name}/revision/${encodeURIComponent( + params.revision ?? "HEAD" + )}?${search.toString()}`, { headers: { ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), diff --git a/packages/hub/src/lib/download-file-to-cache-dir.spec.ts b/packages/hub/src/lib/download-file-to-cache-dir.spec.ts new file mode 100644 index 000000000..879a4d406 --- /dev/null +++ b/packages/hub/src/lib/download-file-to-cache-dir.spec.ts @@ -0,0 +1,248 @@ +import { expect, test, describe, vi, beforeEach } from "vitest"; +import type { RepoDesignation, RepoId } from "../types/public"; +import { dirname, join } from "node:path"; +import { lstat, mkdir, stat, symlink, writeFile, rename } from "node:fs/promises"; +import { pathsInfo } from "./paths-info"; +import type { Stats } from "node:fs"; +import { getHFHubCachePath, getRepoFolderName } from "./cache-management"; +import { toRepoId } from "../utils/toRepoId"; +import { downloadFileToCacheDir } from "./download-file-to-cache-dir"; + +vi.mock("node:fs/promises", () => ({ + writeFile: vi.fn(), + rename: vi.fn(), + symlink: vi.fn(), + lstat: vi.fn(), + mkdir: vi.fn(), + stat: vi.fn(), +})); + +vi.mock("./paths-info", () => ({ + pathsInfo: vi.fn(), +})); + +const DUMMY_REPO: RepoId = { + name: "hello-world", + type: "model", +}; + +const DUMMY_ETAG = "dummy-etag"; + +// utility test method to get blob file path +function _getBlobFile(params: { + repo: RepoDesignation; + etag: string; + cacheDir?: string; // default to {@link getHFHubCache} +}) { + return join(params.cacheDir ?? getHFHubCachePath(), getRepoFolderName(toRepoId(params.repo)), "blobs", params.etag); +} + +// utility test method to get snapshot file path +function _getSnapshotFile(params: { + repo: RepoDesignation; + path: string; + revision: string; + cacheDir?: string; // default to {@link getHFHubCache} +}) { + return join( + params.cacheDir ?? getHFHubCachePath(), + getRepoFolderName(toRepoId(params.repo)), + "snapshots", + params.revision, + params.path + ); +} + +describe("downloadFileToCacheDir", () => { + const fetchMock: typeof fetch = vi.fn(); + beforeEach(() => { + vi.resetAllMocks(); + // mock 200 request + vi.mocked(fetchMock).mockResolvedValue({ + status: 200, + ok: true, + body: "dummy-body", + } as unknown as Response); + + // prevent to use caching + vi.mocked(stat).mockRejectedValue(new Error("Do not exists")); + vi.mocked(lstat).mockRejectedValue(new Error("Do not exists")); + }); + + test("should throw an error if fileDownloadInfo return nothing", async () => { + await expect(async () => { + await downloadFileToCacheDir({ + repo: DUMMY_REPO, + path: "/README.md", + fetch: fetchMock, + }); + }).rejects.toThrowError("cannot get path info for /README.md"); + + expect(pathsInfo).toHaveBeenCalledWith( + expect.objectContaining({ + repo: DUMMY_REPO, + paths: ["/README.md"], + fetch: fetchMock, + }) + ); + }); + + test("existing symlinked and blob should not re-download it", async () => { + // ///snapshots/README.md + const expectPointer = _getSnapshotFile({ + repo: DUMMY_REPO, + path: "/README.md", + revision: "dd4bc8b21efa05ec961e3efc4ee5e3832a3679c7", + }); + // stat ensure a symlink and the pointed file exists + vi.mocked(stat).mockResolvedValue({} as Stats); // prevent default mocked reject + + const output = await downloadFileToCacheDir({ + repo: DUMMY_REPO, + path: "/README.md", + fetch: fetchMock, + revision: "dd4bc8b21efa05ec961e3efc4ee5e3832a3679c7", + }); + + expect(stat).toHaveBeenCalledOnce(); + // Get call argument for stat + const starArg = vi.mocked(stat).mock.calls[0][0]; + + expect(starArg).toBe(expectPointer); + expect(fetchMock).not.toHaveBeenCalledWith(); + + expect(output).toBe(expectPointer); + }); + + test("existing blob should only create the symlink", async () => { + // ///snapshots/README.md + const expectPointer = _getSnapshotFile({ + repo: DUMMY_REPO, + path: "/README.md", + revision: "dummy-commit-hash", + }); + // //blobs/ + const expectedBlob = _getBlobFile({ + repo: DUMMY_REPO, + etag: DUMMY_ETAG, + }); + + // mock existing blob only no symlink + vi.mocked(lstat).mockResolvedValue({} as Stats); + // mock pathsInfo resolve content + vi.mocked(pathsInfo).mockResolvedValue([ + { + oid: DUMMY_ETAG, + size: 55, + path: "README.md", + type: "file", + lastCommit: { + date: new Date(), + id: "dummy-commit-hash", + title: "Commit msg", + }, + }, + ]); + + const output = await downloadFileToCacheDir({ + repo: DUMMY_REPO, + path: "/README.md", + fetch: fetchMock, + }); + + expect(stat).not.toHaveBeenCalled(); + // should have check for the blob + expect(lstat).toHaveBeenCalled(); + expect(vi.mocked(lstat).mock.calls[0][0]).toBe(expectedBlob); + + // symlink should have been created + expect(symlink).toHaveBeenCalledOnce(); + // no download done + expect(fetchMock).not.toHaveBeenCalled(); + + expect(output).toBe(expectPointer); + }); + + test("expect resolve value to be the pointer path of downloaded file", async () => { + // ///snapshots/README.md + const expectPointer = _getSnapshotFile({ + repo: DUMMY_REPO, + path: "/README.md", + revision: "dummy-commit-hash", + }); + // //blobs/ + const expectedBlob = _getBlobFile({ + repo: DUMMY_REPO, + etag: DUMMY_ETAG, + }); + + vi.mocked(pathsInfo).mockResolvedValue([ + { + oid: DUMMY_ETAG, + size: 55, + path: "README.md", + type: "file", + lastCommit: { + date: new Date(), + id: "dummy-commit-hash", + title: "Commit msg", + }, + }, + ]); + + const output = await downloadFileToCacheDir({ + repo: DUMMY_REPO, + path: "/README.md", + fetch: fetchMock, + }); + + // expect blobs and snapshots folder to have been mkdir + expect(vi.mocked(mkdir).mock.calls[0][0]).toBe(dirname(expectedBlob)); + expect(vi.mocked(mkdir).mock.calls[1][0]).toBe(dirname(expectPointer)); + + expect(output).toBe(expectPointer); + }); + + test("should write fetch response to blob", async () => { + // ///snapshots/README.md + const expectPointer = _getSnapshotFile({ + repo: DUMMY_REPO, + path: "/README.md", + revision: "dummy-commit-hash", + }); + // //blobs/ + const expectedBlob = _getBlobFile({ + repo: DUMMY_REPO, + etag: DUMMY_ETAG, + }); + + // mock pathsInfo resolve content + vi.mocked(pathsInfo).mockResolvedValue([ + { + oid: DUMMY_ETAG, + size: 55, + path: "README.md", + type: "file", + lastCommit: { + date: new Date(), + id: "dummy-commit-hash", + title: "Commit msg", + }, + }, + ]); + + await downloadFileToCacheDir({ + repo: DUMMY_REPO, + path: "/README.md", + fetch: fetchMock, + }); + + const incomplete = `${expectedBlob}.incomplete`; + // 1. should write fetch#response#body to incomplete file + expect(writeFile).toHaveBeenCalledWith(incomplete, "dummy-body"); + // 2. should rename the incomplete to the blob expected name + expect(rename).toHaveBeenCalledWith(incomplete, expectedBlob); + // 3. should create symlink pointing to blob + expect(symlink).toHaveBeenCalledWith(expectedBlob, expectPointer); + }); +}); diff --git a/packages/hub/src/lib/download-file-to-cache-dir.ts b/packages/hub/src/lib/download-file-to-cache-dir.ts new file mode 100644 index 000000000..92c4edbf0 --- /dev/null +++ b/packages/hub/src/lib/download-file-to-cache-dir.ts @@ -0,0 +1,129 @@ +import { getHFHubCachePath, getRepoFolderName } from "./cache-management"; +import { dirname, join } from "node:path"; +import { writeFile, rename, symlink, lstat, mkdir, stat } from "node:fs/promises"; +import type { CommitInfo, PathInfo } from "./paths-info"; +import { pathsInfo } from "./paths-info"; +import type { CredentialsParams, RepoDesignation } from "../types/public"; +import { toRepoId } from "../utils/toRepoId"; +import { downloadFile } from "./download-file"; + +export const REGEX_COMMIT_HASH: RegExp = new RegExp("^[0-9a-f]{40}$"); + +function getFilePointer(storageFolder: string, revision: string, relativeFilename: string): string { + const snapshotPath = join(storageFolder, "snapshots"); + return join(snapshotPath, revision, relativeFilename); +} + +/** + * handy method to check if a file exists, or the pointer of a symlinks exists + * @param path + * @param followSymlinks + */ +async function exists(path: string, followSymlinks?: boolean): Promise { + try { + if (followSymlinks) { + await stat(path); + } else { + await lstat(path); + } + return true; + } catch (err: unknown) { + return false; + } +} + +/** + * Download a given file if it's not already present in the local cache. + * @param params + * @return the symlink to the blob object + */ +export async function downloadFileToCacheDir( + params: { + repo: RepoDesignation; + path: string; + /** + * If true, will download the raw git file. + * + * For example, when calling on a file stored with Git LFS, the pointer file will be downloaded instead. + */ + raw?: boolean; + /** + * An optional Git revision id which can be a branch name, a tag, or a commit hash. + * + * @default "main" + */ + revision?: string; + hubUrl?: string; + cacheDir?: string; + /** + * Custom fetch function to use instead of the default one, for example to use a proxy or edit headers. + */ + fetch?: typeof fetch; + } & Partial +): Promise { + // get revision provided or default to main + const revision = params.revision ?? "main"; + const cacheDir = params.cacheDir ?? getHFHubCachePath(); + // get repo id + const repoId = toRepoId(params.repo); + // get storage folder + const storageFolder = join(cacheDir, getRepoFolderName(repoId)); + + let commitHash: string | undefined; + + // if user provides a commitHash as revision, and they already have the file on disk, shortcut everything. + if (REGEX_COMMIT_HASH.test(revision)) { + commitHash = revision; + const pointerPath = getFilePointer(storageFolder, revision, params.path); + if (await exists(pointerPath, true)) return pointerPath; + } + + const pathsInformation: (PathInfo & { lastCommit: CommitInfo })[] = await pathsInfo({ + ...params, + paths: [params.path], + revision: revision, + expand: true, + }); + if (!pathsInformation || pathsInformation.length !== 1) throw new Error(`cannot get path info for ${params.path}`); + + let etag: string; + if (pathsInformation[0].lfs) { + etag = pathsInformation[0].lfs.oid; // get the LFS pointed file oid + } else { + etag = pathsInformation[0].oid; // get the repo file if not a LFS pointer + } + + const pointerPath = getFilePointer(storageFolder, commitHash ?? pathsInformation[0].lastCommit.id, params.path); + const blobPath = join(storageFolder, "blobs", etag); + + // mkdir blob and pointer path parent directory + await mkdir(dirname(blobPath), { recursive: true }); + await mkdir(dirname(pointerPath), { recursive: true }); + + // We might already have the blob but not the pointer + // shortcut the download if needed + if (await exists(blobPath)) { + // create symlinks in snapshot folder to blob object + await symlink(blobPath, pointerPath); + return pointerPath; + } + + const incomplete = `${blobPath}.incomplete`; + console.debug(`Downloading ${params.path} to ${incomplete}`); + + const response: Response | null = await downloadFile({ + ...params, + revision: commitHash, + }); + + if (!response || !response.ok || !response.body) throw new Error(`invalid response for file ${params.path}`); + + // @ts-expect-error resp.body is a Stream, but Stream in internal to node + await writeFile(incomplete, response.body); + + // rename .incomplete file to expect blob + await rename(incomplete, blobPath); + // create symlinks in snapshot folder to blob object + await symlink(blobPath, pointerPath); + return pointerPath; +} diff --git a/packages/hub/src/lib/download-file.spec.ts b/packages/hub/src/lib/download-file.spec.ts new file mode 100644 index 000000000..01fc64c94 --- /dev/null +++ b/packages/hub/src/lib/download-file.spec.ts @@ -0,0 +1,65 @@ +import { expect, test, describe, vi } from "vitest"; +import { downloadFile } from "./download-file"; +import type { RepoId } from "../types/public"; + +const DUMMY_REPO: RepoId = { + name: "hello-world", + type: "model", +}; + +describe("downloadFile", () => { + test("hubUrl params should overwrite HUB_URL", async () => { + const fetchMock: typeof fetch = vi.fn(); + vi.mocked(fetchMock).mockResolvedValue({ + status: 200, + ok: true, + } as Response); + + await downloadFile({ + repo: DUMMY_REPO, + path: "/README.md", + hubUrl: "http://dummy-hub", + fetch: fetchMock, + }); + + expect(fetchMock).toHaveBeenCalledWith("http://dummy-hub/hello-world/resolve/main//README.md", expect.anything()); + }); + + test("raw params should use raw url", async () => { + const fetchMock: typeof fetch = vi.fn(); + vi.mocked(fetchMock).mockResolvedValue({ + status: 200, + ok: true, + } as Response); + + await downloadFile({ + repo: DUMMY_REPO, + path: "README.md", + raw: true, + fetch: fetchMock, + }); + + expect(fetchMock).toHaveBeenCalledWith("https://huggingface.co/hello-world/raw/main/README.md", expect.anything()); + }); + + test("internal server error should propagate the error", async () => { + const fetchMock: typeof fetch = vi.fn(); + vi.mocked(fetchMock).mockResolvedValue({ + status: 500, + ok: false, + headers: new Map([["Content-Type", "application/json"]]), + json: () => ({ + error: "Dummy internal error", + }), + } as unknown as Response); + + await expect(async () => { + await downloadFile({ + repo: DUMMY_REPO, + path: "README.md", + raw: true, + fetch: fetchMock, + }); + }).rejects.toThrowError("Dummy internal error"); + }); +}); diff --git a/packages/hub/src/lib/file-download-info.ts b/packages/hub/src/lib/file-download-info.ts index 210bd11e7..3dcc79ee9 100644 --- a/packages/hub/src/lib/file-download-info.ts +++ b/packages/hub/src/lib/file-download-info.ts @@ -50,7 +50,7 @@ export async function fileDownloadInfo( const resp = await (params.fetch ?? fetch)(url, { method: "GET", headers: { - ...(params.credentials && { + ...(accessToken && { Authorization: `Bearer ${accessToken}`, }), Range: "bytes=0-0", diff --git a/packages/hub/src/lib/index.ts b/packages/hub/src/lib/index.ts index 603674ea0..24e239bdc 100644 --- a/packages/hub/src/lib/index.ts +++ b/packages/hub/src/lib/index.ts @@ -8,6 +8,7 @@ export * from "./delete-file"; export * from "./delete-files"; export * from "./delete-repo"; export * from "./download-file"; +export * from "./download-file-to-cache-dir"; export * from "./file-download-info"; export * from "./file-exists"; export * from "./list-commits"; @@ -19,6 +20,8 @@ export * from "./model-info"; export * from "./oauth-handle-redirect"; export * from "./oauth-login-url"; export * from "./parse-safetensors-metadata"; +export * from "./paths-info"; +export * from "./snapshot-download"; export * from "./space-info"; export * from "./upload-file"; export * from "./upload-files"; diff --git a/packages/hub/src/lib/list-datasets.ts b/packages/hub/src/lib/list-datasets.ts index 5bfbc4a15..fecfa8c32 100644 --- a/packages/hub/src/lib/list-datasets.ts +++ b/packages/hub/src/lib/list-datasets.ts @@ -86,7 +86,7 @@ export async function* listDatasets< const res: Response = await (params?.fetch ?? fetch)(url, { headers: { accept: "application/json", - ...(params?.credentials ? { Authorization: `Bearer ${accessToken}` } : undefined), + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined), }, }); diff --git a/packages/hub/src/lib/list-files.spec.ts b/packages/hub/src/lib/list-files.spec.ts index 83b4438b3..ee59a78a0 100644 --- a/packages/hub/src/lib/list-files.spec.ts +++ b/packages/hub/src/lib/list-files.spec.ts @@ -75,7 +75,7 @@ describe("listFiles", () => { const files: ListFileEntry[] = []; for await (const entry of cursor) { - delete entry.security; // flaky + delete entry.securityFileStatus; // flaky files.push(entry); } diff --git a/packages/hub/src/lib/list-files.ts b/packages/hub/src/lib/list-files.ts index 3083e9587..1648bfe60 100644 --- a/packages/hub/src/lib/list-files.ts +++ b/packages/hub/src/lib/list-files.ts @@ -28,7 +28,7 @@ export interface ListFileEntry { /** * Only fetched if `expand` is set to `true` in the `listFiles` call. */ - security?: unknown; + securityFileStatus?: unknown; } /** @@ -48,7 +48,7 @@ export async function* listFiles( */ path?: string; /** - * Fetch `lastCommit` and `securityStatus` for each file. + * Fetch `lastCommit` and `securityFileStatus` for each file. */ expand?: boolean; revision?: string; diff --git a/packages/hub/src/lib/list-models.ts b/packages/hub/src/lib/list-models.ts index 0e62c8caf..db241c458 100644 --- a/packages/hub/src/lib/list-models.ts +++ b/packages/hub/src/lib/list-models.ts @@ -94,7 +94,7 @@ export async function* listModels< const res: Response = await (params?.fetch ?? fetch)(url, { headers: { accept: "application/json", - ...(params?.credentials ? { Authorization: `Bearer ${accessToken}` } : undefined), + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined), }, }); diff --git a/packages/hub/src/lib/list-spaces.ts b/packages/hub/src/lib/list-spaces.ts index 36a251afd..a14e7e301 100644 --- a/packages/hub/src/lib/list-spaces.ts +++ b/packages/hub/src/lib/list-spaces.ts @@ -82,7 +82,7 @@ export async function* listSpaces< const res: Response = await (params?.fetch ?? fetch)(url, { headers: { accept: "application/json", - ...(params?.credentials ? { Authorization: `Bearer ${accessToken}` } : undefined), + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined), }, }); diff --git a/packages/hub/src/lib/model-info.spec.ts b/packages/hub/src/lib/model-info.spec.ts index cdbad71c1..3657e964d 100644 --- a/packages/hub/src/lib/model-info.spec.ts +++ b/packages/hub/src/lib/model-info.spec.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; import { modelInfo } from "./model-info"; +import type { ModelEntry } from "./list-models"; +import type { ApiModelInfo } from "../types/api/api-model"; describe("modelInfo", () => { it("should return the model info", async () => { @@ -17,4 +19,41 @@ describe("modelInfo", () => { private: false, }); }); + + it("should return the model info with author", async () => { + const info: ModelEntry & Pick = await modelInfo({ + name: "openai-community/gpt2", + additionalFields: ["author"], + }); + expect(info).toEqual({ + id: "621ffdc036468d709f17434d", + downloads: expect.any(Number), + author: "openai-community", + gated: false, + name: "openai-community/gpt2", + updatedAt: expect.any(Date), + likes: expect.any(Number), + task: "text-generation", + private: false, + }); + }); + + it("should return the model info for a specific revision", async () => { + const info: ModelEntry & Pick = await modelInfo({ + name: "openai-community/gpt2", + additionalFields: ["sha"], + revision: "f27b190eeac4c2302d24068eabf5e9d6044389ae", + }); + expect(info).toEqual({ + id: "621ffdc036468d709f17434d", + downloads: expect.any(Number), + gated: false, + name: "openai-community/gpt2", + updatedAt: expect.any(Date), + likes: expect.any(Number), + task: "text-generation", + private: false, + sha: "f27b190eeac4c2302d24068eabf5e9d6044389ae", + }); + }); }); diff --git a/packages/hub/src/lib/model-info.ts b/packages/hub/src/lib/model-info.ts index 2744905c0..4e4291c3b 100644 --- a/packages/hub/src/lib/model-info.ts +++ b/packages/hub/src/lib/model-info.ts @@ -7,12 +7,16 @@ import { pick } from "../utils/pick"; import { MODEL_EXPAND_KEYS, type MODEL_EXPANDABLE_KEYS, type ModelEntry } from "./list-models"; export async function modelInfo< - const T extends Exclude<(typeof MODEL_EXPANDABLE_KEYS)[number], (typeof MODEL_EXPANDABLE_KEYS)[number]> = never, + const T extends Exclude<(typeof MODEL_EXPANDABLE_KEYS)[number], (typeof MODEL_EXPAND_KEYS)[number]> = never, >( params: { name: string; hubUrl?: string; additionalFields?: T[]; + /** + * An optional Git revision id which can be a branch name, a tag, or a commit hash. + */ + revision?: string; /** * Custom fetch function to use instead of the default one, for example to use a proxy or edit headers. */ @@ -27,7 +31,9 @@ export async function modelInfo< ]).toString(); const response = await (params.fetch || fetch)( - `${params?.hubUrl || HUB_URL}/api/models/${params.name}?${search.toString()}`, + `${params?.hubUrl || HUB_URL}/api/models/${params.name}/revision/${encodeURIComponent( + params.revision ?? "HEAD" + )}?${search.toString()}`, { headers: { ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), diff --git a/packages/hub/src/lib/oauth-handle-redirect.spec.ts b/packages/hub/src/lib/oauth-handle-redirect.spec.ts new file mode 100644 index 000000000..f06f6a40e --- /dev/null +++ b/packages/hub/src/lib/oauth-handle-redirect.spec.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; +import { TEST_COOKIE, TEST_HUB_URL } from "../test/consts"; +import { oauthLoginUrl } from "./oauth-login-url"; +import { oauthHandleRedirect } from "./oauth-handle-redirect"; + +describe("oauthHandleRedirect", () => { + it("should work", async () => { + const localStorage = { + nonce: undefined, + codeVerifier: undefined, + }; + const url = await oauthLoginUrl({ + clientId: "dummy-app", + redirectUrl: "http://localhost:3000", + localStorage, + scopes: "openid profile email", + hubUrl: TEST_HUB_URL, + }); + const resp = await fetch(url, { + method: "POST", + headers: { + Cookie: `token=${TEST_COOKIE}`, + }, + redirect: "manual", + }); + if (resp.status !== 303) { + throw new Error(`Failed to fetch url ${url}: ${resp.status} ${resp.statusText}`); + } + const location = resp.headers.get("Location"); + if (!location) { + throw new Error(`No location header in response`); + } + const result = await oauthHandleRedirect({ + redirectedUrl: location, + codeVerifier: localStorage.codeVerifier, + nonce: localStorage.nonce, + hubUrl: TEST_HUB_URL, + }); + + if (!result) { + throw new Error("Expected result to be defined"); + } + expect(result.accessToken).toEqual(expect.any(String)); + expect(result.accessTokenExpiresAt).toBeInstanceOf(Date); + expect(result.accessTokenExpiresAt.getTime()).toBeGreaterThan(Date.now()); + expect(result.scope).toEqual(expect.any(String)); + expect(result.userInfo).toEqual({ + sub: "62f264b9f3c90f4b6514a269", + name: "@huggingface/hub CI bot", + preferred_username: "hub.js", + email_verified: true, + email: "eliott@huggingface.co", + isPro: false, + picture: "https://hub-ci.huggingface.co/avatars/934b830e9fdaa879487852f79eef7165.svg", + profile: "https://hub-ci.huggingface.co/hub.js", + website: "https://github.com/huggingface/hub.js", + orgs: [], + }); + }); +}); diff --git a/packages/hub/src/lib/oauth-handle-redirect.ts b/packages/hub/src/lib/oauth-handle-redirect.ts index 771b6d439..4df6311ec 100644 --- a/packages/hub/src/lib/oauth-handle-redirect.ts +++ b/packages/hub/src/lib/oauth-handle-redirect.ts @@ -1,28 +1,100 @@ import { HUB_URL } from "../consts"; import { createApiError } from "../error"; +export interface UserInfo { + /** + * OpenID Connect field. Unique identifier for the user, even in case of rename. + */ + sub: string; + /** + * OpenID Connect field. The user's full name. + */ + name: string; + /** + * OpenID Connect field. The user's username. + */ + preferred_username: string; + /** + * OpenID Connect field, available if scope "email" was granted. + */ + email_verified?: boolean; + /** + * OpenID Connect field, available if scope "email" was granted. + */ + email?: string; + /** + * OpenID Connect field. The user's profile picture URL. + */ + picture: string; + /** + * OpenID Connect field. The user's profile URL. + */ + profile: string; + /** + * OpenID Connect field. The user's website URL. + */ + website?: string; + + /** + * Hugging Face field. Whether the user is a pro user. + */ + isPro: boolean; + /** + * Hugging Face field. Whether the user has a payment method set up. Needs "read-billing" scope. + */ + canPay?: boolean; + /** + * Hugging Face field. The user's orgs + */ + orgs?: Array<{ + /** + * OpenID Connect field. Unique identifier for the org. + */ + sub: string; + /** + * OpenID Connect field. The org's full name. + */ + name: string; + /** + * OpenID Connect field. The org's username. + */ + preferred_username: string; + /** + * OpenID Connect field. The org's profile picture URL. + */ + picture: string; + + /** + * Hugging Face field. Whether the org is an enterprise org. + */ + isEnterprise: boolean; + /** + * Hugging Face field. Whether the org has a payment method set up. Needs "read-billing" scope, and the user needs to approve access to the org in the OAuth page. + */ + canPay?: boolean; + /** + * Hugging Face field. The user's role in the org. The user needs to approve access to the org in the OAuth page. + */ + roleInOrg?: string; + /** + * HuggingFace field. When the user granted the oauth app access to the org, but didn't complete SSO. + * + * Should never happen directly after the oauth flow. + */ + pendingSSO?: boolean; + /** + * HuggingFace field. When the user granted the oauth app access to the org, but didn't complete MFA. + * + * Should never happen directly after the oauth flow. + */ + missingMFA?: boolean; + }>; +} + export interface OAuthResult { accessToken: string; accessTokenExpiresAt: Date; - userInfo: { - id: string; - name: string; - fullname: string; - email?: string; - emailVerified?: boolean; - avatarUrl: string; - websiteUrl?: string; - isPro: boolean; - canPay?: boolean; - orgs: Array<{ - id: string; - name: string; - isEnterprise: boolean; - canPay?: boolean; - avatarUrl: string; - roleInOrg?: string; - }>; - }; + userInfo: UserInfo; /** * State passed to the OAuth provider in the original request to the OAuth provider. */ @@ -39,12 +111,47 @@ export interface OAuthResult { * There is also a helper function {@link oauthHandleRedirectIfPresent}, which will call `oauthHandleRedirect` if the URL contains an oauth code * in the query parameters and return `false` otherwise. */ -export async function oauthHandleRedirect(opts?: { hubUrl?: string }): Promise { - if (typeof window === "undefined") { - throw new Error("oauthHandleRedirect is only available in the browser"); +export async function oauthHandleRedirect(opts?: { + /** + * The URL of the hub. Defaults to {@link HUB_URL}. + */ + hubUrl?: string; + /** + * The URL to analyze. + * + * @default window.location.href + */ + redirectedUrl?: string; + /** + * nonce generated by oauthLoginUrl + * + * @default localStorage.getItem("huggingface.co:oauth:nonce") + */ + nonce?: string; + /** + * codeVerifier generated by oauthLoginUrl + * + * @default localStorage.getItem("huggingface.co:oauth:code_verifier") + */ + codeVerifier?: string; +}): Promise { + if (typeof window === "undefined" && !opts?.redirectedUrl) { + throw new Error("oauthHandleRedirect is only available in the browser, unless you provide redirectedUrl"); + } + if (typeof localStorage === "undefined" && (!opts?.nonce || !opts?.codeVerifier)) { + throw new Error( + "oauthHandleRedirect requires localStorage to be available, unless you provide nonce and codeVerifier" + ); } - const searchParams = new URLSearchParams(window.location.search); + const redirectedUrl = opts?.redirectedUrl ?? window.location.href; + const searchParams = (() => { + try { + return new URL(redirectedUrl).searchParams; + } catch (err) { + throw new Error("Failed to parse redirected URL: " + redirectedUrl); + } + })(); const [error, errorDescription] = [searchParams.get("error"), searchParams.get("error_description")]; @@ -53,17 +160,17 @@ export async function oauthHandleRedirect(opts?: { hubUrl?: string }): Promise; - } = await userInfoRes.json(); + const userInfo: UserInfo = await userInfoRes.json(); return { accessToken: token.access_token, accessTokenExpiresAt, - userInfo: { - id: userInfo.sub, - name: userInfo.name, - fullname: userInfo.preferred_username, - email: userInfo.email, - emailVerified: userInfo.email_verified, - avatarUrl: userInfo.picture, - websiteUrl: userInfo.website, - isPro: userInfo.isPro, - orgs: - userInfo.orgs?.map((org) => ({ - id: org.sub, - name: org.name, - fullname: org.name, - isEnterprise: org.isEnterprise, - canPay: org.canPay, - avatarUrl: org.picture, - roleInOrg: org.roleInOrg, - })) ?? [], - }, + userInfo: userInfo, state: parsedState.state, scope: token.scope, }; @@ -207,12 +281,39 @@ export async function oauthHandleRedirect(opts?: { hubUrl?: string }): Promise { - if (typeof window === "undefined") { - throw new Error("oauthHandleRedirect is only available in the browser"); +export async function oauthHandleRedirectIfPresent(opts?: { + /** + * The URL of the hub. Defaults to {@link HUB_URL}. + */ + hubUrl?: string; + /** + * The URL to analyze. + * + * @default window.location.href + */ + redirectedUrl?: string; + /** + * nonce generated by oauthLoginUrl + * + * @default localStorage.getItem("huggingface.co:oauth:nonce") + */ + nonce?: string; + /** + * codeVerifier generated by oauthLoginUrl + * + * @default localStorage.getItem("huggingface.co:oauth:code_verifier") + */ + codeVerifier?: string; +}): Promise { + if (typeof window === "undefined" && !opts?.redirectedUrl) { + throw new Error("oauthHandleRedirect is only available in the browser, unless you provide redirectedUrl"); } - - const searchParams = new URLSearchParams(window.location.search); + if (typeof localStorage === "undefined" && (!opts?.nonce || !opts?.codeVerifier)) { + throw new Error( + "oauthHandleRedirect requires localStorage to be available, unless you provide nonce and codeVerifier" + ); + } + const searchParams = new URLSearchParams(opts?.redirectedUrl ?? window.location.search); if (searchParams.has("error")) { return oauthHandleRedirect(opts); diff --git a/packages/hub/src/lib/oauth-login-url.ts b/packages/hub/src/lib/oauth-login-url.ts index 9067ba994..9b8bca3f3 100644 --- a/packages/hub/src/lib/oauth-login-url.ts +++ b/packages/hub/src/lib/oauth-login-url.ts @@ -40,7 +40,7 @@ export async function oauthLoginUrl(opts?: { clientId?: string; hubUrl?: string; /** - * OAuth scope, a list of space separate scopes. + * OAuth scope, a list of space-separated scopes. * * For static Spaces, you can omit this and it will be loaded from the Space config, as long as `hf_oauth: true` is present in the README.md's metadata. * For other Spaces, it is available to the backend in the OAUTH_SCOPES environment variable, as long as `hf_oauth: true` is present in the README.md's metadata. @@ -64,9 +64,24 @@ export async function oauthLoginUrl(opts?: { * State to pass to the OAuth provider, which will be returned in the call to `oauthLogin` after the redirect. */ state?: string; + /** + * If provided, will be filled with the code verifier and nonce used for the OAuth flow, + * instead of using localStorage. + * + * When calling {@link `oauthHandleRedirectIfPresent`} or {@link `oauthHandleRedirect`} you will need to provide the same values. + */ + localStorage?: { + codeVerifier?: string; + nonce?: string; + }; }): Promise { - if (typeof window === "undefined") { - throw new Error("oauthLogin is only available in the browser"); + if (typeof window === "undefined" && (!opts?.redirectUrl || !opts?.clientId)) { + throw new Error("oauthLogin is only available in the browser, unless you provide clientId and redirectUrl"); + } + if (typeof localStorage === "undefined" && !opts?.localStorage) { + throw new Error( + "oauthLogin requires localStorage to be available in the context, unless you provide a localStorage empty object as argument" + ); } const hubUrl = opts?.hubUrl || HUB_URL; @@ -91,18 +106,37 @@ export async function oauthLoginUrl(opts?: { // Two random UUIDs concatenated together, because min length is 43 and max length is 128 const newCodeVerifier = globalThis.crypto.randomUUID() + globalThis.crypto.randomUUID(); - localStorage.setItem("huggingface.co:oauth:nonce", newNonce); - localStorage.setItem("huggingface.co:oauth:code_verifier", newCodeVerifier); + if (opts?.localStorage) { + if (opts.localStorage.codeVerifier !== undefined && opts.localStorage.codeVerifier !== null) { + throw new Error( + "localStorage.codeVerifier must be a initially set to null or undefined, and will be filled by oauthLoginUrl" + ); + } + if (opts.localStorage.nonce !== undefined && opts.localStorage.nonce !== null) { + throw new Error( + "localStorage.nonce must be a initially set to null or undefined, and will be filled by oauthLoginUrl" + ); + } + opts.localStorage.codeVerifier = newCodeVerifier; + opts.localStorage.nonce = newNonce; + } else { + localStorage.setItem("huggingface.co:oauth:nonce", newNonce); + localStorage.setItem("huggingface.co:oauth:code_verifier", newCodeVerifier); + } - const redirectUri = opts?.redirectUrl || window.location.href; + const redirectUri = opts?.redirectUrl || (typeof window !== "undefined" ? window.location.href : undefined); + if (!redirectUri) { + throw new Error("Missing redirectUrl"); + } const state = JSON.stringify({ nonce: newNonce, redirectUri, state: opts?.state, }); - // @ts-expect-error window.huggingface is defined inside static Spaces. - const variables: Record | null = window?.huggingface?.variables ?? null; + const variables: Record | null = + // @ts-expect-error window.huggingface is defined inside static Spaces. + typeof window !== "undefined" ? window.huggingface?.variables ?? null : null; const clientId = opts?.clientId || variables?.OAUTH_CLIENT_ID; diff --git a/packages/hub/src/lib/paths-info.spec.ts b/packages/hub/src/lib/paths-info.spec.ts new file mode 100644 index 000000000..837f4a192 --- /dev/null +++ b/packages/hub/src/lib/paths-info.spec.ts @@ -0,0 +1,75 @@ +import { expect, it, describe } from "vitest"; +import type { CommitInfo, PathInfo, SecurityFileStatus } from "./paths-info"; +import { pathsInfo } from "./paths-info"; + +describe("pathsInfo", () => { + it("should fetch LFS path info", async () => { + const result: PathInfo[] = await pathsInfo({ + repo: { + name: "bert-base-uncased", + type: "model", + }, + paths: ["tf_model.h5"], + revision: "dd4bc8b21efa05ec961e3efc4ee5e3832a3679c7", + }); + + expect(result).toHaveLength(1); + + const modelPathInfo = result[0]; + expect(modelPathInfo.path).toBe("tf_model.h5"); + expect(modelPathInfo.type).toBe("file"); + // lfs pointer, therefore lfs should be defined + expect(modelPathInfo?.lfs).toBeDefined(); + expect(modelPathInfo?.lfs?.oid).toBe("a7a17d6d844b5de815ccab5f42cad6d24496db3850a2a43d8258221018ce87d2"); + expect(modelPathInfo?.lfs?.size).toBe(536063208); + expect(modelPathInfo?.lfs?.pointerSize).toBe(134); + + // should not include expand info + expect(modelPathInfo.lastCommit).toBeUndefined(); + expect(modelPathInfo.securityFileStatus).toBeUndefined(); + }); + + it("expand parmas should fetch lastCommit and securityFileStatus", async () => { + const result: (PathInfo & { + lastCommit: CommitInfo; + securityFileStatus: SecurityFileStatus; + })[] = await pathsInfo({ + repo: { + name: "bert-base-uncased", + type: "model", + }, + paths: ["tf_model.h5"], + revision: "dd4bc8b21efa05ec961e3efc4ee5e3832a3679c7", + expand: true, // include + }); + + expect(result).toHaveLength(1); + + const modelPathInfo = result[0]; + + // should include expand info + expect(modelPathInfo.lastCommit).toBeDefined(); + expect(modelPathInfo.securityFileStatus).toBeDefined(); + + expect(modelPathInfo.lastCommit.id).toBe("dd4bc8b21efa05ec961e3efc4ee5e3832a3679c7"); + expect(modelPathInfo.lastCommit.title).toBe("Update tf_model.h5"); + expect(modelPathInfo.lastCommit.date.getTime()).toBe(1569268124000); // 2019-09-23T19:48:44.000Z + }); + + it("non-LFS pointer should have lfs undefined", async () => { + const result: PathInfo[] = await pathsInfo({ + repo: { + name: "bert-base-uncased", + type: "model", + }, + paths: ["config.json"], + revision: "dd4bc8b21efa05ec961e3efc4ee5e3832a3679c7", + }); + + expect(result).toHaveLength(1); + + const modelPathInfo = result[0]; + expect(modelPathInfo.path).toBe("config.json"); + expect(modelPathInfo.lfs).toBeUndefined(); + }); +}); diff --git a/packages/hub/src/lib/paths-info.ts b/packages/hub/src/lib/paths-info.ts new file mode 100644 index 000000000..ec455f882 --- /dev/null +++ b/packages/hub/src/lib/paths-info.ts @@ -0,0 +1,124 @@ +import type { CredentialsParams, RepoDesignation } from "../types/public"; +import { checkCredentials } from "../utils/checkCredentials"; +import { toRepoId } from "../utils/toRepoId"; +import { HUB_URL } from "../consts"; +import { createApiError } from "../error"; + +export interface LfsPathInfo { + oid: string; + size: number; + pointerSize: number; +} + +export interface CommitInfo { + id: string; + title: string; + date: Date; +} + +export interface SecurityFileStatus { + status: string; +} + +export interface PathInfo { + path: string; + type: string; + oid: string; + size: number; + /** + * Only defined when path is LFS pointer + */ + lfs?: LfsPathInfo; + lastCommit?: CommitInfo; + securityFileStatus?: SecurityFileStatus; +} + +// Define the overloaded signatures +export function pathsInfo( + params: { + repo: RepoDesignation; + paths: string[]; + expand: true; // if expand true + revision?: string; + hubUrl?: string; + /** + * Custom fetch function to use instead of the default one, for example to use a proxy or edit headers. + */ + fetch?: typeof fetch; + } & Partial +): Promise<(PathInfo & { lastCommit: CommitInfo; securityFileStatus: SecurityFileStatus })[]>; +export function pathsInfo( + params: { + repo: RepoDesignation; + paths: string[]; + expand?: boolean; + revision?: string; + hubUrl?: string; + /** + * Custom fetch function to use instead of the default one, for example to use a proxy or edit headers. + */ + fetch?: typeof fetch; + } & Partial +): Promise; + +export async function pathsInfo( + params: { + repo: RepoDesignation; + paths: string[]; + expand?: boolean; + revision?: string; + hubUrl?: string; + /** + * Custom fetch function to use instead of the default one, for example to use a proxy or edit headers. + */ + fetch?: typeof fetch; + } & Partial +): Promise { + const accessToken = checkCredentials(params); + const repoId = toRepoId(params.repo); + + const hubUrl = params.hubUrl ?? HUB_URL; + + const url = `${hubUrl}/api/${repoId.type}s/${repoId.name}/paths-info/${encodeURIComponent( + params.revision ?? "main" + )}`; + + const resp = await (params.fetch ?? fetch)(url, { + method: "POST", + headers: { + ...(accessToken && { + Authorization: `Bearer ${accessToken}`, + }), + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + paths: params.paths, + expand: params.expand, + }), + }); + + if (!resp.ok) { + throw await createApiError(resp); + } + + const json: unknown = await resp.json(); + if (!Array.isArray(json)) throw new Error("malformed response: expected array"); + + return json.map((item: PathInfo) => ({ + path: item.path, + lfs: item.lfs, + type: item.type, + oid: item.oid, + size: item.size, + // expand fields + securityFileStatus: item.securityFileStatus, + lastCommit: item.lastCommit + ? { + date: new Date(item.lastCommit.date), + title: item.lastCommit.title, + id: item.lastCommit.id, + } + : undefined, + })); +} diff --git a/packages/hub/src/lib/snapshot-download.spec.ts b/packages/hub/src/lib/snapshot-download.spec.ts new file mode 100644 index 000000000..0c44cc842 --- /dev/null +++ b/packages/hub/src/lib/snapshot-download.spec.ts @@ -0,0 +1,275 @@ +import { expect, test, describe, vi, beforeEach } from "vitest"; +import { dirname, join } from "node:path"; +import { mkdir, writeFile } from "node:fs/promises"; +import { getHFHubCachePath } from "./cache-management"; +import { downloadFileToCacheDir } from "./download-file-to-cache-dir"; +import { snapshotDownload } from "./snapshot-download"; +import type { ListFileEntry } from "./list-files"; +import { listFiles } from "./list-files"; +import { modelInfo } from "./model-info"; +import type { ModelEntry } from "./list-models"; +import type { ApiModelInfo } from "../types/api/api-model"; +import { datasetInfo } from "./dataset-info"; +import type { DatasetEntry } from "./list-datasets"; +import type { ApiDatasetInfo } from "../types/api/api-dataset"; +import { spaceInfo } from "./space-info"; +import type { SpaceEntry } from "./list-spaces"; +import type { ApiSpaceInfo } from "../types/api/api-space"; + +vi.mock("node:fs/promises", () => ({ + writeFile: vi.fn(), + mkdir: vi.fn(), +})); + +vi.mock("./space-info", () => ({ + spaceInfo: vi.fn(), +})); + +vi.mock("./dataset-info", () => ({ + datasetInfo: vi.fn(), +})); + +vi.mock("./model-info", () => ({ + modelInfo: vi.fn(), +})); + +vi.mock("./list-files", () => ({ + listFiles: vi.fn(), +})); + +vi.mock("./download-file-to-cache-dir", () => ({ + downloadFileToCacheDir: vi.fn(), +})); + +const DUMMY_SHA = "dummy-sha"; + +// utility method to transform an array of ListFileEntry to an AsyncGenerator +async function* toAsyncGenerator(content: ListFileEntry[]): AsyncGenerator { + for (const entry of content) { + yield Promise.resolve(entry); + } +} + +beforeEach(() => { + vi.resetAllMocks(); + vi.mocked(listFiles).mockReturnValue(toAsyncGenerator([])); + + // mock repo info + vi.mocked(modelInfo).mockResolvedValue({ + sha: DUMMY_SHA, + } as ModelEntry & ApiModelInfo); + vi.mocked(datasetInfo).mockResolvedValue({ + sha: DUMMY_SHA, + } as DatasetEntry & ApiDatasetInfo); + vi.mocked(spaceInfo).mockResolvedValue({ + sha: DUMMY_SHA, + } as SpaceEntry & ApiSpaceInfo); +}); + +describe("snapshotDownload", () => { + test("empty AsyncGenerator should not call downloadFileToCacheDir", async () => { + await snapshotDownload({ + repo: { + name: "foo/bar", + type: "space", + }, + }); + + expect(downloadFileToCacheDir).not.toHaveBeenCalled(); + }); + + test("repo type model should use modelInfo", async () => { + await snapshotDownload({ + repo: { + name: "foo/bar", + type: "model", + }, + }); + expect(modelInfo).toHaveBeenCalledOnce(); + expect(modelInfo).toHaveBeenCalledWith({ + name: "foo/bar", + additionalFields: ["sha"], + revision: "main", + repo: { + name: "foo/bar", + type: "model", + }, + }); + }); + + test("repo type dataset should use datasetInfo", async () => { + await snapshotDownload({ + repo: { + name: "foo/bar", + type: "dataset", + }, + }); + expect(datasetInfo).toHaveBeenCalledOnce(); + expect(datasetInfo).toHaveBeenCalledWith({ + name: "foo/bar", + additionalFields: ["sha"], + revision: "main", + repo: { + name: "foo/bar", + type: "dataset", + }, + }); + }); + + test("repo type space should use spaceInfo", async () => { + await snapshotDownload({ + repo: { + name: "foo/bar", + type: "space", + }, + }); + expect(spaceInfo).toHaveBeenCalledOnce(); + expect(spaceInfo).toHaveBeenCalledWith({ + name: "foo/bar", + additionalFields: ["sha"], + revision: "main", + repo: { + name: "foo/bar", + type: "space", + }, + }); + }); + + test("commitHash should be saved to ref folder", async () => { + await snapshotDownload({ + repo: { + name: "foo/bar", + type: "space", + }, + revision: "dummy-revision", + }); + + // cross-platform testing + const expectedPath = join(getHFHubCachePath(), "spaces--foo--bar", "refs", "dummy-revision"); + expect(mkdir).toHaveBeenCalledWith(dirname(expectedPath), { recursive: true }); + expect(writeFile).toHaveBeenCalledWith(expectedPath, DUMMY_SHA); + }); + + test("directory ListFileEntry should mkdir it", async () => { + vi.mocked(listFiles).mockReturnValue( + toAsyncGenerator([ + { + oid: "dummy-etag", + type: "directory", + path: "potatoes", + size: 0, + lastCommit: { + date: new Date().toISOString(), + id: DUMMY_SHA, + title: "feat: best commit", + }, + }, + ]) + ); + + await snapshotDownload({ + repo: { + name: "foo/bar", + type: "space", + }, + }); + + // cross-platform testing + const expectedPath = join(getHFHubCachePath(), "spaces--foo--bar", "snapshots", DUMMY_SHA, "potatoes"); + expect(mkdir).toHaveBeenCalledWith(expectedPath, { recursive: true }); + }); + + test("files in ListFileEntry should download them", async () => { + const entries: ListFileEntry[] = Array.from({ length: 10 }, (_, i) => ({ + oid: `dummy-etag-${i}`, + type: "file", + path: `file-${i}.txt`, + size: i, + lastCommit: { + date: new Date().toISOString(), + id: DUMMY_SHA, + title: "feat: best commit", + }, + })); + vi.mocked(listFiles).mockReturnValue(toAsyncGenerator(entries)); + + await snapshotDownload({ + repo: { + name: "foo/bar", + type: "space", + }, + }); + + for (const entry of entries) { + expect(downloadFileToCacheDir).toHaveBeenCalledWith( + expect.objectContaining({ + repo: { + name: "foo/bar", + type: "space", + }, + path: entry.path, + revision: DUMMY_SHA, + }) + ); + } + }); + + test("custom params should be propagated", async () => { + // fetch mock + const fetchMock: typeof fetch = vi.fn(); + const hubMock = "https://foor.bar"; + const accessTokenMock = "dummy-access-token"; + + vi.mocked(listFiles).mockReturnValue( + toAsyncGenerator([ + { + oid: `dummy-etag`, + type: "file", + path: `file.txt`, + size: 10, + lastCommit: { + date: new Date().toISOString(), + id: DUMMY_SHA, + title: "feat: best commit", + }, + }, + ]) + ); + + await snapshotDownload({ + repo: { + name: "foo/bar", + type: "space", + }, + hubUrl: hubMock, + fetch: fetchMock, + accessToken: accessTokenMock, + }); + + expect(spaceInfo).toHaveBeenCalledWith( + expect.objectContaining({ + fetch: fetchMock, + hubUrl: hubMock, + accessToken: accessTokenMock, + }) + ); + + // list files should receive custom fetch + expect(listFiles).toHaveBeenCalledWith( + expect.objectContaining({ + fetch: fetchMock, + hubUrl: hubMock, + accessToken: accessTokenMock, + }) + ); + + // download file to cache should receive custom fetch + expect(downloadFileToCacheDir).toHaveBeenCalledWith( + expect.objectContaining({ + fetch: fetchMock, + hubUrl: hubMock, + accessToken: accessTokenMock, + }) + ); + }); +}); diff --git a/packages/hub/src/lib/snapshot-download.ts b/packages/hub/src/lib/snapshot-download.ts new file mode 100644 index 000000000..b3e30c13f --- /dev/null +++ b/packages/hub/src/lib/snapshot-download.ts @@ -0,0 +1,124 @@ +import type { CredentialsParams, RepoDesignation } from "../types/public"; +import { listFiles } from "./list-files"; +import { getHFHubCachePath, getRepoFolderName } from "./cache-management"; +import { spaceInfo } from "./space-info"; +import { datasetInfo } from "./dataset-info"; +import { modelInfo } from "./model-info"; +import { toRepoId } from "../utils/toRepoId"; +import { join, dirname } from "node:path"; +import { mkdir, writeFile } from "node:fs/promises"; +import { downloadFileToCacheDir } from "./download-file-to-cache-dir"; + +export const DEFAULT_REVISION = "main"; + +/** + * Downloads an entire repository at a given revision in the cache directory {@link getHFHubCachePath}. + * You can list all cached repositories using {@link scanCachedRepo} + * @remarks It uses internally {@link downloadFileToCacheDir}. + */ +export async function snapshotDownload( + params: { + repo: RepoDesignation; + cacheDir?: string; + /** + * An optional Git revision id which can be a branch name, a tag, or a commit hash. + * + * @default "main" + */ + revision?: string; + hubUrl?: string; + /** + * Custom fetch function to use instead of the default one, for example to use a proxy or edit headers. + */ + fetch?: typeof fetch; + } & Partial +): Promise { + let cacheDir: string; + if (params.cacheDir) { + cacheDir = params.cacheDir; + } else { + cacheDir = getHFHubCachePath(); + } + + let revision: string; + if (params.revision) { + revision = params.revision; + } else { + revision = DEFAULT_REVISION; + } + + const repoId = toRepoId(params.repo); + + // get repository revision value (sha) + let repoInfo: { sha: string }; + switch (repoId.type) { + case "space": + repoInfo = await spaceInfo({ + ...params, + name: repoId.name, + additionalFields: ["sha"], + revision: revision, + }); + break; + case "dataset": + repoInfo = await datasetInfo({ + ...params, + name: repoId.name, + additionalFields: ["sha"], + revision: revision, + }); + break; + case "model": + repoInfo = await modelInfo({ + ...params, + name: repoId.name, + additionalFields: ["sha"], + revision: revision, + }); + break; + default: + throw new Error(`invalid repository type ${repoId.type}`); + } + + const commitHash: string = repoInfo.sha; + + // get storage folder + const storageFolder = join(cacheDir, getRepoFolderName(repoId)); + const snapshotFolder = join(storageFolder, "snapshots", commitHash); + + // if passed revision is not identical to commit_hash + // then revision has to be a branch name or tag name. + // In that case store a ref. + if (revision !== commitHash) { + const refPath = join(storageFolder, "refs", revision); + await mkdir(dirname(refPath), { recursive: true }); + await writeFile(refPath, commitHash); + } + + const cursor = listFiles({ + ...params, + repo: params.repo, + recursive: true, + revision: repoInfo.sha, + }); + + for await (const entry of cursor) { + switch (entry.type) { + case "file": + await downloadFileToCacheDir({ + ...params, + path: entry.path, + revision: commitHash, + cacheDir: cacheDir, + }); + break; + case "directory": + await mkdir(join(snapshotFolder, entry.path), { recursive: true }); + break; + default: + throw new Error(`unknown entry type: ${entry.type}`); + } + } + + return snapshotFolder; +} diff --git a/packages/hub/src/lib/space-info.spec.ts b/packages/hub/src/lib/space-info.spec.ts index 1974a04c4..ea966f98b 100644 --- a/packages/hub/src/lib/space-info.spec.ts +++ b/packages/hub/src/lib/space-info.spec.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; import { spaceInfo } from "./space-info"; +import type { SpaceEntry } from "./list-spaces"; +import type { ApiSpaceInfo } from "../types/api/api-space"; describe("spaceInfo", () => { it("should return the space info", async () => { @@ -15,4 +17,37 @@ describe("spaceInfo", () => { sdk: "static", }); }); + + it("should return the space info with author", async () => { + const info: SpaceEntry & Pick = await spaceInfo({ + name: "huggingfacejs/client-side-oauth", + additionalFields: ["author"], + }); + expect(info).toEqual({ + id: "659835e689010f9c7aed608d", + name: "huggingfacejs/client-side-oauth", + updatedAt: expect.any(Date), + likes: expect.any(Number), + private: false, + sdk: "static", + author: "huggingfacejs", + }); + }); + + it("should return the space info for a given revision", async () => { + const info: SpaceEntry & Pick = await spaceInfo({ + name: "huggingfacejs/client-side-oauth", + additionalFields: ["sha"], + revision: "e410a9ff348e6bed393b847711e793282d7c672e", + }); + expect(info).toEqual({ + id: "659835e689010f9c7aed608d", + name: "huggingfacejs/client-side-oauth", + updatedAt: expect.any(Date), + likes: expect.any(Number), + private: false, + sdk: "static", + sha: "e410a9ff348e6bed393b847711e793282d7c672e", + }); + }); }); diff --git a/packages/hub/src/lib/space-info.ts b/packages/hub/src/lib/space-info.ts index a1b5516a6..942235382 100644 --- a/packages/hub/src/lib/space-info.ts +++ b/packages/hub/src/lib/space-info.ts @@ -14,6 +14,10 @@ export async function spaceInfo< name: string; hubUrl?: string; additionalFields?: T[]; + /** + * An optional Git revision id which can be a branch name, a tag, or a commit hash. + */ + revision?: string; /** * Custom fetch function to use instead of the default one, for example to use a proxy or edit headers. */ @@ -28,7 +32,9 @@ export async function spaceInfo< ]).toString(); const response = await (params.fetch || fetch)( - `${params?.hubUrl || HUB_URL}/api/spaces/${params.name}?${search.toString()}`, + `${params?.hubUrl || HUB_URL}/api/spaces/${params.name}/revision/${encodeURIComponent( + params.revision ?? "HEAD" + )}?${search.toString()}`, { headers: { ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), diff --git a/packages/hub/src/test/consts.ts b/packages/hub/src/test/consts.ts index 297ea01d0..6b8b7983d 100644 --- a/packages/hub/src/test/consts.ts +++ b/packages/hub/src/test/consts.ts @@ -1,3 +1,4 @@ export const TEST_HUB_URL = "https://hub-ci.huggingface.co"; export const TEST_USER = "hub.js"; export const TEST_ACCESS_TOKEN = "hf_hub.js"; +export const TEST_COOKIE = "huggingface-hub.js-cookie"; diff --git a/packages/hub/src/types/api/api-commit.ts b/packages/hub/src/types/api/api-commit.ts index 404ad5494..b5fbfec4b 100644 --- a/packages/hub/src/types/api/api-commit.ts +++ b/packages/hub/src/types/api/api-commit.ts @@ -5,7 +5,7 @@ export interface ApiLfsBatchRequest { /** * Optional object describing the server ref that the objects belong to. Note: Added in v2.4. * - * We use this object for QOL and to fail early for users when they're tring to push to the wrong reference. + * We use this object for QOL and to fail early for users when they're trying to push to the wrong reference. * But it does nothing for security. */ ref?: { diff --git a/packages/hub/vitest-browser.config.mts b/packages/hub/vitest-browser.config.mts index e106a2fba..2c21588af 100644 --- a/packages/hub/vitest-browser.config.mts +++ b/packages/hub/vitest-browser.config.mts @@ -2,6 +2,14 @@ import { configDefaults, defineConfig } from "vitest/config"; export default defineConfig({ test: { - exclude: [...configDefaults.exclude, "src/utils/FileBlob.spec.ts", "src/lib/cache-management.spec.ts"], + exclude: [ + ...configDefaults.exclude, + "src/utils/FileBlob.spec.ts", + "src/lib/cache-management.spec.ts", + "src/lib/download-file-to-cache-dir.spec.ts", + "src/lib/snapshot-download.spec.ts", + // Because we use redirect: "manual" in the test + "src/lib/oauth-handle-redirect.spec.ts", + ], }, }); diff --git a/packages/jinja/package.json b/packages/jinja/package.json index e62dcd802..b70794ab1 100644 --- a/packages/jinja/package.json +++ b/packages/jinja/package.json @@ -1,7 +1,7 @@ { "name": "@huggingface/jinja", "packageManager": "pnpm@8.10.5", - "version": "0.3.1", + "version": "0.3.2", "description": "A minimalistic JavaScript implementation of the Jinja templating engine, specifically designed for parsing and rendering ML chat templates.", "repository": "https://github.com/huggingface/huggingface.js.git", "publishConfig": { diff --git a/packages/jinja/pnpm-lock.yaml b/packages/jinja/pnpm-lock.yaml index a6ecf2e33..11e24490c 100644 --- a/packages/jinja/pnpm-lock.yaml +++ b/packages/jinja/pnpm-lock.yaml @@ -16,9 +16,9 @@ devDependencies: version: 5.3.2 packages: - /@emnapi/runtime@1.3.1: - resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + resolution: + { integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw== } requiresBuild: true dependencies: tslib: 2.8.0 @@ -26,12 +26,14 @@ packages: optional: true /@huggingface/jinja@0.3.1: - resolution: {integrity: sha512-SbcBWUKDQ76lzlVYOloscUk0SJjuL1LcbZsfQv/Bxxc7dwJMYuS+DAQ+HhVw6ZkTFXArejaX5HQRuCuleYwYdA==} - engines: {node: ">=18"} + resolution: + { integrity: sha512-SbcBWUKDQ76lzlVYOloscUk0SJjuL1LcbZsfQv/Bxxc7dwJMYuS+DAQ+HhVw6ZkTFXArejaX5HQRuCuleYwYdA== } + engines: { node: ">=18" } dev: true /@huggingface/transformers@3.0.0: - resolution: {integrity: sha512-OWIPnTijAw4DQ+IFHBOrej2SDdYyykYlTtpTLCEt5MZq/e9Cb65RS2YVhdGcgbaW/6JAL3i8ZA5UhDeWGm4iRQ==} + resolution: + { integrity: sha512-OWIPnTijAw4DQ+IFHBOrej2SDdYyykYlTtpTLCEt5MZq/e9Cb65RS2YVhdGcgbaW/6JAL3i8ZA5UhDeWGm4iRQ== } dependencies: "@huggingface/jinja": 0.3.1 onnxruntime-node: 1.19.2 @@ -40,8 +42,9 @@ packages: dev: true /@img/sharp-darwin-arm64@0.33.5: - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + resolution: + { integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ== } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm64] os: [darwin] requiresBuild: true @@ -51,8 +54,9 @@ packages: optional: true /@img/sharp-darwin-x64@0.33.5: - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + resolution: + { integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q== } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] os: [darwin] requiresBuild: true @@ -62,7 +66,8 @@ packages: optional: true /@img/sharp-libvips-darwin-arm64@1.0.4: - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + resolution: + { integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg== } cpu: [arm64] os: [darwin] requiresBuild: true @@ -70,7 +75,8 @@ packages: optional: true /@img/sharp-libvips-darwin-x64@1.0.4: - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + resolution: + { integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ== } cpu: [x64] os: [darwin] requiresBuild: true @@ -78,7 +84,8 @@ packages: optional: true /@img/sharp-libvips-linux-arm64@1.0.4: - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + resolution: + { integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA== } cpu: [arm64] os: [linux] requiresBuild: true @@ -86,7 +93,8 @@ packages: optional: true /@img/sharp-libvips-linux-arm@1.0.5: - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + resolution: + { integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g== } cpu: [arm] os: [linux] requiresBuild: true @@ -94,7 +102,8 @@ packages: optional: true /@img/sharp-libvips-linux-s390x@1.0.4: - resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + resolution: + { integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA== } cpu: [s390x] os: [linux] requiresBuild: true @@ -102,7 +111,8 @@ packages: optional: true /@img/sharp-libvips-linux-x64@1.0.4: - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + resolution: + { integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw== } cpu: [x64] os: [linux] requiresBuild: true @@ -110,7 +120,8 @@ packages: optional: true /@img/sharp-libvips-linuxmusl-arm64@1.0.4: - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + resolution: + { integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA== } cpu: [arm64] os: [linux] requiresBuild: true @@ -118,7 +129,8 @@ packages: optional: true /@img/sharp-libvips-linuxmusl-x64@1.0.4: - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + resolution: + { integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw== } cpu: [x64] os: [linux] requiresBuild: true @@ -126,8 +138,9 @@ packages: optional: true /@img/sharp-linux-arm64@0.33.5: - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + resolution: + { integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA== } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm64] os: [linux] requiresBuild: true @@ -137,8 +150,9 @@ packages: optional: true /@img/sharp-linux-arm@0.33.5: - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + resolution: + { integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ== } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm] os: [linux] requiresBuild: true @@ -148,8 +162,9 @@ packages: optional: true /@img/sharp-linux-s390x@0.33.5: - resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + resolution: + { integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q== } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [s390x] os: [linux] requiresBuild: true @@ -159,8 +174,9 @@ packages: optional: true /@img/sharp-linux-x64@0.33.5: - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + resolution: + { integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA== } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] os: [linux] requiresBuild: true @@ -170,8 +186,9 @@ packages: optional: true /@img/sharp-linuxmusl-arm64@0.33.5: - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + resolution: + { integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g== } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm64] os: [linux] requiresBuild: true @@ -181,8 +198,9 @@ packages: optional: true /@img/sharp-linuxmusl-x64@0.33.5: - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + resolution: + { integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw== } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] os: [linux] requiresBuild: true @@ -192,8 +210,9 @@ packages: optional: true /@img/sharp-wasm32@0.33.5: - resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + resolution: + { integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg== } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [wasm32] requiresBuild: true dependencies: @@ -202,8 +221,9 @@ packages: optional: true /@img/sharp-win32-ia32@0.33.5: - resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + resolution: + { integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ== } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [ia32] os: [win32] requiresBuild: true @@ -211,8 +231,9 @@ packages: optional: true /@img/sharp-win32-x64@0.33.5: - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + resolution: + { integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg== } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] os: [win32] requiresBuild: true @@ -220,8 +241,9 @@ packages: optional: true /@isaacs/cliui@8.0.2: - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: ">=12"} + resolution: + { integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== } + engines: { node: ">=12" } dependencies: string-width: 5.1.2 string-width-cjs: /string-width@4.2.3 @@ -232,134 +254,159 @@ packages: dev: true /@isaacs/fs-minipass@4.0.1: - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: ">=18.0.0"} + resolution: + { integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== } + engines: { node: ">=18.0.0" } dependencies: minipass: 7.1.2 dev: true /@pkgjs/parseargs@0.11.0: - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: ">=14"} + resolution: + { integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== } + engines: { node: ">=14" } requiresBuild: true dev: true optional: true /@protobufjs/aspromise@1.1.2: - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + resolution: + { integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== } dev: true /@protobufjs/base64@1.1.2: - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + resolution: + { integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== } dev: true /@protobufjs/codegen@2.0.4: - resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + resolution: + { integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== } dev: true /@protobufjs/eventemitter@1.1.0: - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + resolution: + { integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== } dev: true /@protobufjs/fetch@1.1.0: - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + resolution: + { integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== } dependencies: "@protobufjs/aspromise": 1.1.2 "@protobufjs/inquire": 1.1.0 dev: true /@protobufjs/float@1.0.2: - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + resolution: + { integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== } dev: true /@protobufjs/inquire@1.1.0: - resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + resolution: + { integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== } dev: true /@protobufjs/path@1.1.2: - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + resolution: + { integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== } dev: true /@protobufjs/pool@1.1.0: - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + resolution: + { integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== } dev: true /@protobufjs/utf8@1.1.0: - resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + resolution: + { integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== } dev: true /@types/node@20.9.4: - resolution: {integrity: sha512-wmyg8HUhcn6ACjsn8oKYjkN/zUzQeNtMy44weTJSM6p4MMzEOuKbA3OjJ267uPCOW7Xex9dyrNTful8XTQYoDA==} + resolution: + { integrity: sha512-wmyg8HUhcn6ACjsn8oKYjkN/zUzQeNtMy44weTJSM6p4MMzEOuKbA3OjJ267uPCOW7Xex9dyrNTful8XTQYoDA== } dependencies: undici-types: 5.26.5 dev: true /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: ">=8"} + resolution: + { integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== } + engines: { node: ">=8" } dev: true /ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} - engines: {node: ">=12"} + resolution: + { integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== } + engines: { node: ">=12" } dev: true /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: ">=8"} + resolution: + { integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== } + engines: { node: ">=8" } dependencies: color-convert: 2.0.1 dev: true /ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: ">=12"} + resolution: + { integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== } + engines: { node: ">=12" } dev: true /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + resolution: + { integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== } dev: true /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + resolution: + { integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== } dependencies: balanced-match: 1.0.2 dev: true /chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: ">=18"} + resolution: + { integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== } + engines: { node: ">=18" } dev: true /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: ">=7.0.0"} + resolution: + { integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== } + engines: { node: ">=7.0.0" } dependencies: color-name: 1.1.4 dev: true /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + resolution: + { integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== } dev: true /color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + resolution: + { integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== } dependencies: color-name: 1.1.4 simple-swizzle: 0.2.2 dev: true /color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: ">=12.5.0"} + resolution: + { integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== } + engines: { node: ">=12.5.0" } dependencies: color-convert: 2.0.1 color-string: 1.9.1 dev: true /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: ">= 8"} + resolution: + { integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== } + engines: { node: ">= 8" } dependencies: path-key: 3.1.1 shebang-command: 2.0.0 @@ -367,36 +414,43 @@ packages: dev: true /detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: ">=8"} + resolution: + { integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== } + engines: { node: ">=8" } dev: true /eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + resolution: + { integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== } dev: true /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + resolution: + { integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== } dev: true /emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + resolution: + { integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== } dev: true /flatbuffers@1.12.0: - resolution: {integrity: sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==} + resolution: + { integrity: sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ== } dev: true /foreground-child@3.3.0: - resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} - engines: {node: ">=14"} + resolution: + { integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== } + engines: { node: ">=14" } dependencies: cross-spawn: 7.0.3 signal-exit: 4.1.0 dev: true /glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + resolution: + { integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== } hasBin: true dependencies: foreground-child: 3.3.0 @@ -408,24 +462,29 @@ packages: dev: true /guid-typescript@1.0.9: - resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} + resolution: + { integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ== } dev: true /is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + resolution: + { integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== } dev: true /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: ">=8"} + resolution: + { integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== } + engines: { node: ">=8" } dev: true /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + resolution: + { integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== } dev: true /jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + resolution: + { integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== } dependencies: "@isaacs/cliui": 8.0.2 optionalDependencies: @@ -433,49 +492,58 @@ packages: dev: true /long@5.2.3: - resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + resolution: + { integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== } dev: true /lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + resolution: + { integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== } dev: true /minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: ">=16 || 14 >=14.17"} + resolution: + { integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== } + engines: { node: ">=16 || 14 >=14.17" } dependencies: brace-expansion: 2.0.1 dev: true /minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: ">=16 || 14 >=14.17"} + resolution: + { integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== } + engines: { node: ">=16 || 14 >=14.17" } dev: true /minizlib@3.0.1: - resolution: {integrity: sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==} - engines: {node: ">= 18"} + resolution: + { integrity: sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg== } + engines: { node: ">= 18" } dependencies: minipass: 7.1.2 rimraf: 5.0.10 dev: true /mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: ">=10"} + resolution: + { integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== } + engines: { node: ">=10" } hasBin: true dev: true /onnxruntime-common@1.19.2: - resolution: {integrity: sha512-a4R7wYEVFbZBlp0BfhpbFWqe4opCor3KM+5Wm22Az3NGDcQMiU2hfG/0MfnBs+1ZrlSGmlgWeMcXQkDk1UFb8Q==} + resolution: + { integrity: sha512-a4R7wYEVFbZBlp0BfhpbFWqe4opCor3KM+5Wm22Az3NGDcQMiU2hfG/0MfnBs+1ZrlSGmlgWeMcXQkDk1UFb8Q== } dev: true /onnxruntime-common@1.20.0-dev.20241016-2b8fc5529b: - resolution: {integrity: sha512-KZK8b6zCYGZFjd4ANze0pqBnqnFTS3GIVeclQpa2qseDpXrCQJfkWBixRcrZShNhm3LpFOZ8qJYFC5/qsJK9WQ==} + resolution: + { integrity: sha512-KZK8b6zCYGZFjd4ANze0pqBnqnFTS3GIVeclQpa2qseDpXrCQJfkWBixRcrZShNhm3LpFOZ8qJYFC5/qsJK9WQ== } dev: true /onnxruntime-node@1.19.2: - resolution: {integrity: sha512-9eHMP/HKbbeUcqte1JYzaaRC8JPn7ojWeCeoyShO86TOR97OCyIyAIOGX3V95ErjslVhJRXY8Em/caIUc0hm1Q==} + resolution: + { integrity: sha512-9eHMP/HKbbeUcqte1JYzaaRC8JPn7ojWeCeoyShO86TOR97OCyIyAIOGX3V95ErjslVhJRXY8Em/caIUc0hm1Q== } os: [win32, darwin, linux] requiresBuild: true dependencies: @@ -484,7 +552,8 @@ packages: dev: true /onnxruntime-web@1.20.0-dev.20241016-2b8fc5529b: - resolution: {integrity: sha512-1XovqtgqeEFtupuyzdDQo7Tqj4GRyNHzOoXjapCEo4rfH3JrXok5VtqucWfRXHPsOI5qoNxMQ9VE+drDIp6woQ==} + resolution: + { integrity: sha512-1XovqtgqeEFtupuyzdDQo7Tqj4GRyNHzOoXjapCEo4rfH3JrXok5VtqucWfRXHPsOI5qoNxMQ9VE+drDIp6woQ== } dependencies: flatbuffers: 1.12.0 guid-typescript: 1.0.9 @@ -495,29 +564,34 @@ packages: dev: true /package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + resolution: + { integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== } dev: true /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: ">=8"} + resolution: + { integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== } + engines: { node: ">=8" } dev: true /path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: ">=16 || 14 >=14.18"} + resolution: + { integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== } + engines: { node: ">=16 || 14 >=14.18" } dependencies: lru-cache: 10.4.3 minipass: 7.1.2 dev: true /platform@1.3.6: - resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + resolution: + { integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== } dev: true /protobufjs@7.4.0: - resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==} - engines: {node: ">=12.0.0"} + resolution: + { integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw== } + engines: { node: ">=12.0.0" } requiresBuild: true dependencies: "@protobufjs/aspromise": 1.1.2 @@ -535,21 +609,24 @@ packages: dev: true /rimraf@5.0.10: - resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + resolution: + { integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ== } hasBin: true dependencies: glob: 10.4.5 dev: true /semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: ">=10"} + resolution: + { integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== } + engines: { node: ">=10" } hasBin: true dev: true /sharp@0.33.5: - resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + resolution: + { integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw== } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } requiresBuild: true dependencies: color: 4.2.3 @@ -578,31 +655,36 @@ packages: dev: true /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: ">=8"} + resolution: + { integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== } + engines: { node: ">=8" } dependencies: shebang-regex: 3.0.0 dev: true /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: ">=8"} + resolution: + { integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== } + engines: { node: ">=8" } dev: true /signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: ">=14"} + resolution: + { integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== } + engines: { node: ">=14" } dev: true /simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + resolution: + { integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== } dependencies: is-arrayish: 0.3.2 dev: true /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: ">=8"} + resolution: + { integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== } + engines: { node: ">=8" } dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 @@ -610,8 +692,9 @@ packages: dev: true /string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: ">=12"} + resolution: + { integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== } + engines: { node: ">=12" } dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 @@ -619,22 +702,25 @@ packages: dev: true /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: ">=8"} + resolution: + { integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== } + engines: { node: ">=8" } dependencies: ansi-regex: 5.0.1 dev: true /strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: ">=12"} + resolution: + { integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== } + engines: { node: ">=12" } dependencies: ansi-regex: 6.1.0 dev: true /tar@7.4.3: - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} - engines: {node: ">=18"} + resolution: + { integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw== } + engines: { node: ">=18" } dependencies: "@isaacs/fs-minipass": 4.0.1 chownr: 3.0.0 @@ -645,32 +731,37 @@ packages: dev: true /tslib@2.8.0: - resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==} + resolution: + { integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== } requiresBuild: true dev: true optional: true /typescript@5.3.2: - resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==} - engines: {node: ">=14.17"} + resolution: + { integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ== } + engines: { node: ">=14.17" } hasBin: true dev: true /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + resolution: + { integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== } dev: true /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: ">= 8"} + resolution: + { integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== } + engines: { node: ">= 8" } hasBin: true dependencies: isexe: 2.0.0 dev: true /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: ">=10"} + resolution: + { integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== } + engines: { node: ">=10" } dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 @@ -678,8 +769,9 @@ packages: dev: true /wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: ">=12"} + resolution: + { integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== } + engines: { node: ">=12" } dependencies: ansi-styles: 6.2.1 string-width: 5.1.2 @@ -687,6 +779,7 @@ packages: dev: true /yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: ">=18"} + resolution: + { integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== } + engines: { node: ">=18" } dev: true diff --git a/packages/jinja/src/runtime.ts b/packages/jinja/src/runtime.ts index a35221c0a..474de75f7 100644 --- a/packages/jinja/src/runtime.ts +++ b/packages/jinja/src/runtime.ts @@ -274,7 +274,8 @@ export class Environment { ["string", (operand) => operand.type === "StringValue"], ["number", (operand) => operand.type === "NumericValue"], ["integer", (operand) => operand.type === "NumericValue" && Number.isInteger((operand as NumericValue).value)], - ["iterable", (operand) => operand instanceof ArrayValue || operand instanceof StringValue], + ["iterable", (operand) => operand.type === "ArrayValue" || operand.type === "StringValue"], + ["mapping", (operand) => operand.type === "ObjectValue"], [ "lower", (operand) => { @@ -613,12 +614,15 @@ export class Interpreter { if (operand instanceof ArrayValue) { switch (filterName) { - case "selectattr": { + case "selectattr": + case "rejectattr": { + const select = filterName === "selectattr"; + if (operand.value.some((x) => !(x instanceof ObjectValue))) { - throw new Error("`selectattr` can only be applied to array of objects"); + throw new Error(`\`${filterName}\` can only be applied to array of objects`); } if (filter.args.some((x) => x.type !== "StringLiteral")) { - throw new Error("arguments of `selectattr` must be strings"); + throw new Error(`arguments of \`${filterName}\` must be strings`); } const [attr, testName, value] = filter.args.map((x) => this.evaluate(x, environment)) as StringValue[]; @@ -639,10 +643,8 @@ export class Interpreter { // Filter the array using the test function const filtered = (operand.value as ObjectValue[]).filter((item) => { const a = item.value.get(attr.value); - if (a) { - return testFunction(a, value); - } - return false; + const result = a ? testFunction(a, value) : false; + return select ? result : !result; }); return new ArrayValue(filtered); diff --git a/packages/jinja/test/e2e.test.js b/packages/jinja/test/e2e.test.js index fe588e330..833a1a854 100644 --- a/packages/jinja/test/e2e.test.js +++ b/packages/jinja/test/e2e.test.js @@ -76,15 +76,8 @@ const EXAMPLE_FUNCTION_CALLING_WITH_SYSTEM = [ { role: "user", content: "Hi, can you tell me the current stock price of AAPL?" }, ]; -// Adapted from https://huggingface.co/CISCai/Mistral-7B-Instruct-v0.3-SOTA-GGUF -const EXAMPLE_CHAT_WITH_TOOLS = [ - { - role: "user", - content: "What's the weather like in Oslo and Stockholm?", - }, -]; -const EXAMPLE_TOOLS = [ - { +const EXAMPLE_TOOL_JSON_SCHEMAS = { + get_current_weather: { type: "function", function: { name: "get_current_weather", @@ -105,7 +98,81 @@ const EXAMPLE_TOOLS = [ }, }, }, + get_current_temperature_v1: { + type: "function", + function: { + name: "get_current_temperature", + description: "Get the current temperature at a location.", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: 'The location to get the temperature for, in the format "City, Country"', + }, + }, + required: ["location"], + }, + return: { + type: "number", + description: "The current temperature at the specified location in the specified units, as a float.", + }, + }, + }, + get_current_temperature_v2: { + type: "function", + function: { + name: "get_current_temperature", + description: "Get the current temperature at a location.", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: 'The location to get the temperature for, in the format "City, Country"', + }, + unit: { + type: "string", + enum: ["celsius", "fahrenheit"], + description: "The unit to return the temperature in.", + }, + }, + required: ["location", "unit"], + }, + return: { + type: "number", + description: "The current temperature at the specified location in the specified units, as a float.", + }, + }, + }, + get_current_wind_speed: { + type: "function", + function: { + name: "get_current_wind_speed", + description: "Get the current wind speed in km/h at a given location.", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: 'The location to get the temperature for, in the format "City, Country"', + }, + }, + required: ["location"], + }, + return: { + type: "number", + description: "The current wind speed at the given location in km/h, as a float.", + }, + }, + }, +}; + +const EXAMPLE_LIST_OF_TOOLS = [ + EXAMPLE_TOOL_JSON_SCHEMAS.get_current_temperature_v2, + EXAMPLE_TOOL_JSON_SCHEMAS.get_current_wind_speed, ]; + /** * Defined in https://github.com/huggingface/transformers * Keys correspond to `model_type` in the transformers repo. @@ -532,55 +599,7 @@ const TEST_CUSTOM_TEMPLATES = Object.freeze({ }, { role: "tool", tool_call_id: "abcdef123", name: "get_current_temperature", content: "22.0" }, ], - tools: [ - { - type: "function", - function: { - name: "get_current_temperature", - description: "Get the current temperature at a location.", - parameters: { - type: "object", - properties: { - location: { - type: "string", - description: 'The location to get the temperature for, in the format "City, Country"', - }, - unit: { - type: "string", - enum: ["celsius", "fahrenheit"], - description: "The unit to return the temperature in.", - }, - }, - required: ["location", "unit"], - }, - return: { - type: "number", - description: "The current temperature at the specified location in the specified units, as a float.", - }, - }, - }, - { - type: "function", - function: { - name: "get_current_wind_speed", - description: "Get the current wind speed in km/h at a given location.", - parameters: { - type: "object", - properties: { - location: { - type: "string", - description: 'The location to get the temperature for, in the format "City, Country"', - }, - }, - required: ["location"], - }, - return: { - type: "number", - description: "The current wind speed at the given location in km/h, as a float.", - }, - }, - }, - ], + tools: EXAMPLE_LIST_OF_TOOLS, bos_token: "", eos_token: "", }, @@ -590,8 +609,13 @@ const TEST_CUSTOM_TEMPLATES = Object.freeze({ "CISCai/Mistral-7B-Instruct-v0.3-SOTA-GGUF": { chat_template: `{{ bos_token }}{% set ns = namespace(lastuser=-1, system=false, functions=false) %}{% if tools %}{% for message in messages %}{% if message['role'] == 'user' %}{% set ns.lastuser = loop.index0 %}{% elif message['role'] == 'system' %}{% set ns.system = message['content'] %}{% endif %}{% endfor %}{% set ns.functions = tools|selectattr('type','eq','function')|map(attribute='function')|list|tojson %}{% endif %}{% for message in messages %}{% if message['role'] == 'user' %}{% if loop.index0 == ns.lastuser and ns.functions %}{{ '[AVAILABLE_TOOLS] ' }}{{ ns.functions }}{{ '[/AVAILABLE_TOOLS]' }}{% endif %}{{ '[INST] ' }}{% if loop.index0 == ns.lastuser and ns.system %}{{ ns.system + ' ' }}{% endif %}{{ message['content'] }}{{ '[/INST]' }}{% elif message['role'] == 'tool' %}{{ '[TOOL_RESULTS] ' }}{{ dict(call_id=message['tool_call_id'], content=message['content'])|tojson }}{{ '[/TOOL_RESULTS]' }}{% elif message['role'] == 'assistant' %}{% if message['tool_calls'] %}{{ '[TOOL_CALLS] [' }}{% for call in message['tool_calls'] %}{% if call['type'] == 'function' %}{{ dict(id=call['id'], name=call['function']['name'], arguments=call['function']['arguments'])|tojson }}{% endif %}{% if not loop.last %}{{ ', ' }}{% endif %}{% endfor %}{{ ']' }}{% else %}{{ message['content'] }}{% endif %}{{ eos_token }}{% endif %}{% endfor %}`, data: { - messages: EXAMPLE_CHAT_WITH_TOOLS, - tools: EXAMPLE_TOOLS, + messages: [ + { + role: "user", + content: "What's the weather like in Oslo and Stockholm?", + }, + ], + tools: [EXAMPLE_TOOL_JSON_SCHEMAS.get_current_weather], bos_token: "", eos_token: "", }, @@ -630,6 +654,29 @@ const TEST_CUSTOM_TEMPLATES = Object.freeze({ }, target: `<|begin_of_text|>You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: {"type": "function", "function": {"name": get_stock_fundamentals", "description": "get_stock_fundamentals(symbol: str) -> dict - Get fundamental data for a given stock symbol using yfinance API.\n\n Args:\n symbol(str): The stock symbol.\n Returns:\n A dictionary containing fundamental data.\n\nKeys:\n - 'symbol': The stock symbol.\n - 'company_name': The long name of the company.\n - 'sector': The sector to which the company belongs.\n - 'industry': The industry to which the company belongs.\n - 'market_cap': The market capitalization of the company.\n - 'pe_ratio': The forward price-to-earnings ratio.\n - 'pb_ratio': The price-to-book ratio.\n - 'dividend_yield': The dividend yield.\n - 'eps': The trailing earnings per share.\n - 'beta': The beta value of the stock.\n - '52_week_high': The 52-week high price of the stock.\n - '52_week_low': The 52-week low price of the stock.", "parameters": {"type": "object", "properties": {"symbol": {"type": "string", "description": "The stock symbol."}}, "required": ["symbol"]}} Use the following pydantic model json schema for each tool call you will make: {"properties": {"arguments": {"title": "Arguments", "type": "object"}, "name": {"title": "Name", "type": "string"}}, "required": ["arguments", "name"], "title": "FunctionCall", "type": "object"}\nFor each function call return a json object with function name and arguments within XML tags as follows:\n\n{"arguments": , "name": }\n<|im_end|><|im_start|>user\nFetch the stock fundamentals data for Tesla (TSLA)<|im_end|>\n<|im_start|>assistant\n`, }, + "mistralai/Mistral-Nemo-Instruct-2407": { + chat_template: `{%- if messages[0]["role"] == "system" %}\n {%- set system_message = messages[0]["content"] %}\n {%- set loop_messages = messages[1:] %}\n{%- else %}\n {%- set loop_messages = messages %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n{%- set user_messages = loop_messages | selectattr("role", "equalto", "user") | list %}\n\n{%- for message in loop_messages | rejectattr("role", "equalto", "tool") | rejectattr("role", "equalto", "tool_results") | selectattr("tool_calls", "undefined") %}\n {%- if (message["role"] == "user") != (loop.index0 % 2 == 0) %}\n {{- raise_exception("After the optional system message, conversation roles must alternate user/assistant/user/assistant/...") }}\n {%- endif %}\n{%- endfor %}\n\n{{- bos_token }}\n{%- for message in loop_messages %}\n {%- if message["role"] == "user" %}\n {%- if tools is not none and (message == user_messages[-1]) %}\n {{- "[AVAILABLE_TOOLS][" }}\n {%- for tool in tools %}\n {%- set tool = tool.function %}\n {{- '{"type": "function", "function": {' }}\n {%- for key, val in tool.items() if key != "return" %}\n {%- if val is string %}\n {{- '"' + key + '": "' + val + '"' }}\n {%- else %}\n {{- '"' + key + '": ' + val|tojson }}\n {%- endif %}\n {%- if not loop.last %}\n {{- ", " }}\n {%- endif %}\n {%- endfor %}\n {{- "}}" }}\n {%- if not loop.last %}\n {{- ", " }}\n {%- else %}\n {{- "]" }}\n {%- endif %}\n {%- endfor %}\n {{- "[/AVAILABLE_TOOLS]" }}\n {%- endif %}\n {%- if loop.last and system_message is defined %}\n {{- "[INST]" + system_message + "\\n\\n" + message["content"] + "[/INST]" }}\n {%- else %}\n {{- "[INST]" + message["content"] + "[/INST]" }}\n {%- endif %}\n {%- elif message["role"] == "tool_calls" or message.tool_calls is defined %}\n {%- if message.tool_calls is defined %}\n {%- set tool_calls = message.tool_calls %}\n {%- else %}\n {%- set tool_calls = message.content %}\n {%- endif %}\n {{- "[TOOL_CALLS][" }}\n {%- for tool_call in tool_calls %}\n {%- set out = tool_call.function|tojson %}\n {{- out[:-1] }}\n {%- if not tool_call.id is defined or tool_call.id|length != 9 %}\n {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }}\n {%- endif %}\n {{- ', "id": "' + tool_call.id + '"}' }}\n {%- if not loop.last %}\n {{- ", " }}\n {%- else %}\n {{- "]" + eos_token }}\n {%- endif %}\n {%- endfor %}\n {%- elif message["role"] == "assistant" %}\n {{- message["content"] + eos_token}}\n {%- elif message["role"] == "tool_results" or message["role"] == "tool" %}\n {%- if message.content is defined and message.content.content is defined %}\n {%- set content = message.content.content %}\n {%- else %}\n {%- set content = message.content %}\n {%- endif %}\n {{- '[TOOL_RESULTS]{"content": ' + content|string + ", " }}\n {%- if not message.tool_call_id is defined or message.tool_call_id|length != 9 %}\n {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }}\n {%- endif %}\n {{- '"call_id": "' + message.tool_call_id + '"}[/TOOL_RESULTS]' }}\n {%- else %}\n {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }}\n {%- endif %}\n{%- endfor %}\n`, + data: { + messages: EXAMPLE_CHAT, + bos_token: "", + eos_token: "", + }, + target: `[INST]Hello, how are you?[/INST]I'm doing great. How can I help you today?[INST]I'd like to show off how chat templating works![/INST]`, + }, + "meta-llama/Llama-3.1-8B-Instruct": { + chat_template: `{{- bos_token }}\n{%- if custom_tools is defined %}\n {%- set tools = custom_tools %}\n{%- endif %}\n{%- if not tools_in_user_message is defined %}\n {%- set tools_in_user_message = true %}\n{%- endif %}\n{%- if not date_string is defined %}\n {%- set date_string = "26 Jul 2024" %}\n{%- endif %}\n{%- if not tools is defined %}\n {%- set tools = none %}\n{%- endif %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0]['role'] == 'system' %}\n {%- set system_message = messages[0]['content']|trim %}\n {%- set messages = messages[1:] %}\n{%- else %}\n {%- set system_message = "" %}\n{%- endif %}\n\n{#- System message + builtin tools #}\n{{- "<|start_header_id|>system<|end_header_id|>\\n\\n" }}\n{%- if builtin_tools is defined or tools is not none %}\n {{- "Environment: ipython\\n" }}\n{%- endif %}\n{%- if builtin_tools is defined %}\n {{- "Tools: " + builtin_tools | reject('equalto', 'code_interpreter') | join(", ") + "\\n\\n"}}\n{%- endif %}\n{{- "Cutting Knowledge Date: December 2023\\n" }}\n{{- "Today Date: " + date_string + "\\n\\n" }}\n{%- if tools is not none and not tools_in_user_message %}\n {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}\n {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n{%- endif %}\n{{- system_message }}\n{{- "<|eot_id|>" }}\n\n{#- Custom tools are passed in a user message with some extra guidance #}\n{%- if tools_in_user_message and not tools is none %}\n {#- Extract the first user message so we can plug it in here #}\n {%- if messages | length != 0 %}\n {%- set first_user_message = messages[0]['content']|trim %}\n {%- set messages = messages[1:] %}\n {%- else %}\n {{- raise_exception("Cannot put tools in the first user message when there's no first user message!") }}\n{%- endif %}\n {{- '<|start_header_id|>user<|end_header_id|>\\n\\n' -}}\n {{- "Given the following functions, please respond with a JSON for a function call " }}\n {{- "with its proper arguments that best answers the given prompt.\\n\\n" }}\n {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }}\n {{- "Do not use variables.\\n\\n" }}\n {%- for t in tools %}\n {{- t | tojson(indent=4) }}\n {{- "\\n\\n" }}\n {%- endfor %}\n {{- first_user_message + "<|eot_id|>"}}\n{%- endif %}\n\n{%- for message in messages %}\n {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %}\n {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\\n\\n'+ message['content'] | trim + '<|eot_id|>' }}\n {%- elif 'tool_calls' in message %}\n {%- if not message.tool_calls|length == 1 %}\n {{- raise_exception("This model only supports single tool-calls at once!") }}\n {%- endif %}\n {%- set tool_call = message.tool_calls[0].function %}\n {%- if builtin_tools is defined and tool_call.name in builtin_tools %}\n {{- '<|start_header_id|>assistant<|end_header_id|>\\n\\n' -}}\n {{- "<|python_tag|>" + tool_call.name + ".call(" }}\n {%- for arg_name, arg_val in tool_call.arguments | items %}\n {{- arg_name + '="' + arg_val + '"' }}\n {%- if not loop.last %}\n {{- ", " }}\n {%- endif %}\n {%- endfor %}\n {{- ")" }}\n {%- else %}\n {{- '<|start_header_id|>assistant<|end_header_id|>\\n\\n' -}}\n {{- '{"name": "' + tool_call.name + '", ' }}\n {{- '"parameters": ' }}\n {{- tool_call.arguments | tojson }}\n {{- "}" }}\n {%- endif %}\n {%- if builtin_tools is defined %}\n {#- This means we're in ipython mode #}\n {{- "<|eom_id|>" }}\n {%- else %}\n {{- "<|eot_id|>" }}\n {%- endif %}\n {%- elif message.role == "tool" or message.role == "ipython" %}\n {{- "<|start_header_id|>ipython<|end_header_id|>\\n\\n" }}\n {%- if message.content is mapping or message.content is iterable %}\n {{- message.content | tojson }}\n {%- else %}\n {{- message.content }}\n {%- endif %}\n {{- "<|eot_id|>" }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|start_header_id|>assistant<|end_header_id|>\\n\\n' }}\n{%- endif %}\n`, + data: { + messages: [ + { role: "system", content: "You are a bot that responds to weather queries." }, + { role: "user", content: "Hey, what's the temperature in Paris right now?" }, + ], + tools: [EXAMPLE_TOOL_JSON_SCHEMAS.get_current_temperature_v1], + bos_token: "<|begin_of_text|>", + eos_token: "<|im_end|>", + add_generation_prompt: true, + }, + target: `<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\nEnvironment: ipython\nCutting Knowledge Date: December 2023\nToday Date: 26 Jul 2024\n\nYou are a bot that responds to weather queries.<|eot_id|><|start_header_id|>user<|end_header_id|>\n\nGiven the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt.\n\nRespond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables.\n\n{\n "type": "function",\n "function": {\n "name": "get_current_temperature",\n "description": "Get the current temperature at a location.",\n "parameters": {\n "type": "object",\n "properties": {\n "location": {\n "type": "string",\n "description": "The location to get the temperature for, in the format \\"City, Country\\""\n }\n },\n "required": [\n "location"\n ]\n },\n "return": {\n "type": "number",\n "description": "The current temperature at the specified location in the specified units, as a float."\n }\n }\n}\n\nHey, what's the temperature in Paris right now?<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n`, + }, }); describe("End-to-end tests", () => { diff --git a/packages/jinja/test/templates.test.js b/packages/jinja/test/templates.test.js index 7ce91b54b..0a5f9130f 100644 --- a/packages/jinja/test/templates.test.js +++ b/packages/jinja/test/templates.test.js @@ -87,6 +87,8 @@ const TEST_STRINGS = { FILTER_OPERATOR_8: `{{ obj | tojson(indent=2) }}`, FILTER_OPERATOR_9: `{{ data | map(attribute='val') | list | tojson }}`, FILTER_OPERATOR_10: `|{{ " 1 \n 2 \n 3 \n\n " | indent }}|{{ " 1 \n 2 \n 3 \n\n " | indent(2) }}|{{ " 1 \n 2 \n 3 \n\n " | indent(first=True) }}|{{ " 1 \n 2 \n 3 \n\n " | indent(blank=True) }}|{{ " 1 \n 2 \n 3 \n\n " | indent(4, first=True) }}|`, + FILTER_OPERATOR_11: `{{ items | rejectattr('key') | length }}`, + FILTER_OPERATOR_12: `{{ messages | rejectattr('role', 'equalto', 'system') | length }}`, // Logical operators between non-Booleans BOOLEAN_NUMERICAL: `|{{ 1 and 2 }}|{{ 1 and 0 }}|{{ 0 and 1 }}|{{ 0 and 0 }}|{{ 1 or 2 }}|{{ 1 or 0 }}|{{ 0 or 1 }}|{{ 0 or 0 }}|{{ not 1 }}|{{ not 0 }}|`, @@ -101,6 +103,7 @@ const TEST_STRINGS = { IS_OPERATOR_3: `|{{ 1 is odd }}|{{ 2 is odd }}|{{ 1 is even }}|{{ 2 is even }}|{{ 2 is number }}|{{ '2' is number }}|{{ 2 is integer }}|{{ '2' is integer }}|`, IS_OPERATOR_4: `|{{ func is callable }}|{{ 2 is callable }}|{{ 1 is iterable }}|{{ 'hello' is iterable }}|`, IS_OPERATOR_5: `|{{ 'a' is lower }}|{{ 'A' is lower }}|{{ 'a' is upper }}|{{ 'A' is upper }}|`, + IS_OPERATOR_6: `|{{ string is mapping }}|{{ number is mapping }}|{{ array is mapping }}|{{ dict is mapping }}|`, // Short-circuit evaluation SHORT_CIRCUIT: `{{ false and raise_exception('This should not be printed') }}`, @@ -1624,6 +1627,34 @@ const TEST_PARSED = { { value: "}}", type: "CloseExpression" }, { value: "|", type: "Text" }, ], + FILTER_OPERATOR_11: [ + { value: "{{", type: "OpenExpression" }, + { value: "items", type: "Identifier" }, + { value: "|", type: "Pipe" }, + { value: "rejectattr", type: "Identifier" }, + { value: "(", type: "OpenParen" }, + { value: "key", type: "StringLiteral" }, + { value: ")", type: "CloseParen" }, + { value: "|", type: "Pipe" }, + { value: "length", type: "Identifier" }, + { value: "}}", type: "CloseExpression" }, + ], + FILTER_OPERATOR_12: [ + { value: "{{", type: "OpenExpression" }, + { value: "messages", type: "Identifier" }, + { value: "|", type: "Pipe" }, + { value: "rejectattr", type: "Identifier" }, + { value: "(", type: "OpenParen" }, + { value: "role", type: "StringLiteral" }, + { value: ",", type: "Comma" }, + { value: "equalto", type: "StringLiteral" }, + { value: ",", type: "Comma" }, + { value: "system", type: "StringLiteral" }, + { value: ")", type: "CloseParen" }, + { value: "|", type: "Pipe" }, + { value: "length", type: "Identifier" }, + { value: "}}", type: "CloseExpression" }, + ], // Logical operators between non-Booleans BOOLEAN_NUMERICAL: [ @@ -2073,6 +2104,33 @@ const TEST_PARSED = { { value: "}}", type: "CloseExpression" }, { value: "|", type: "Text" }, ], + IS_OPERATOR_6: [ + { value: "|", type: "Text" }, + { value: "{{", type: "OpenExpression" }, + { value: "string", type: "Identifier" }, + { value: "is", type: "Is" }, + { value: "mapping", type: "Identifier" }, + { value: "}}", type: "CloseExpression" }, + { value: "|", type: "Text" }, + { value: "{{", type: "OpenExpression" }, + { value: "number", type: "Identifier" }, + { value: "is", type: "Is" }, + { value: "mapping", type: "Identifier" }, + { value: "}}", type: "CloseExpression" }, + { value: "|", type: "Text" }, + { value: "{{", type: "OpenExpression" }, + { value: "array", type: "Identifier" }, + { value: "is", type: "Is" }, + { value: "mapping", type: "Identifier" }, + { value: "}}", type: "CloseExpression" }, + { value: "|", type: "Text" }, + { value: "{{", type: "OpenExpression" }, + { value: "dict", type: "Identifier" }, + { value: "is", type: "Is" }, + { value: "mapping", type: "Identifier" }, + { value: "}}", type: "CloseExpression" }, + { value: "|", type: "Text" }, + ], // Short-circuit evaluation SHORT_CIRCUIT: [ @@ -2909,6 +2967,12 @@ const TEST_CONTEXT = { data: [{ val: 1 }, { val: 2 }, { val: 3 }], }, FILTER_OPERATOR_10: {}, + FILTER_OPERATOR_11: { + items: [{ key: "a" }, { key: 0 }, { key: 1 }, {}, { key: false }], + }, + FILTER_OPERATOR_12: { + messages: [{ role: "system" }, { role: "user" }, { role: "assistant" }], + }, // Logical operators between non-Booleans BOOLEAN_NUMERICAL: {}, @@ -2927,6 +2991,12 @@ const TEST_CONTEXT = { func: () => {}, }, IS_OPERATOR_5: {}, + IS_OPERATOR_6: { + string: "hello", + number: 1, + array: [1, 2, 3], + dict: { a: 1 }, + }, // Short-circuit evaluation SHORT_CIRCUIT: {}, @@ -3073,6 +3143,8 @@ const EXPECTED_OUTPUTS = { FILTER_OPERATOR_8: `{\n "a": [\n 1,\n 2,\n 3\n ],\n "b": 1,\n "c": {\n "d": 2,\n "e": {\n "f": 3,\n "g": {\n "h": 4,\n "i": [\n 1,\n 2,\n 3\n ]\n }\n }\n }\n}`, FILTER_OPERATOR_9: `[1, 2, 3]`, FILTER_OPERATOR_10: `| 1 \n 2 \n 3 \n\n | 1 \n 2 \n 3 \n\n | 1 \n 2 \n 3 \n\n | 1 \n 2 \n 3 \n \n | 1 \n 2 \n 3 \n\n |`, + FILTER_OPERATOR_11: `3`, + FILTER_OPERATOR_12: `2`, // Logical operators between non-Booleans BOOLEAN_NUMERICAL: `|2|0|0|0|1|1|1|0|false|true|`, @@ -3087,6 +3159,7 @@ const EXPECTED_OUTPUTS = { IS_OPERATOR_3: `|true|false|false|true|true|false|true|false|`, IS_OPERATOR_4: `|true|false|false|true|`, IS_OPERATOR_5: `|true|false|false|true|`, + IS_OPERATOR_6: `|false|false|false|true|`, // Short-circuit evaluation SHORT_CIRCUIT: `false`, diff --git a/packages/tasks-gen/.prettierignore b/packages/tasks-gen/.prettierignore new file mode 100644 index 000000000..cdbeac2bd --- /dev/null +++ b/packages/tasks-gen/.prettierignore @@ -0,0 +1,5 @@ +dist +snippets-fixtures +node_modules +README.md +pnpm-lock.yaml \ No newline at end of file diff --git a/packages/tasks-gen/README.md b/packages/tasks-gen/README.md new file mode 100644 index 000000000..6adbe3211 --- /dev/null +++ b/packages/tasks-gen/README.md @@ -0,0 +1,67 @@ +## @huggingface.js/tasks-gen + +This package is not a published one. It contains scripts that generate or test parts of the `@huggingface.js/tasks` package. + +### generate-snippets-fixtures.ts + +This script generates and tests Inference API snippets. The goal is to have a simple way to review changes in the snippets. +When updating logic in `packages/tasks/src/snippets`, the test snippets must be updated and committed in the same PR. + +To (re-)generate the snippets, run: + +``` +pnpm generate-snippets-fixtures +``` + +If some logic has been updated, you should see the result with a + +``` +git diff +# the diff has to be committed if correct +``` + +To test the snippets, run: + +``` +pnpm test +``` + +Finally if you want to add a test case, you must add an entry in `TEST_CASES` array in `generate-snippets-fixtures.ts`. + +### inference-codegen.ts + +Generates JS and Python dataclasses based on the Inference Specs (jsonschema files). + +This script is run by a cron job once a day and helps getting `@huggingface.js/tasks` and `huggingface_hub` up to date. + +To update the specs manually, run: + +``` +pnpm inference-codegen +``` + +### inference-tei-import.ts + +Fetches TEI specs and generates JSON schema for input and output of text-embeddings (also called feature-extraction). +See https://huggingface.github.io/text-embeddings-inference/ for more details. + +This script is run by a cron job once a day and helps getting `@huggingface.js/tasks` up to date with TEI updates. + +To update the specs manually, run: + +``` +pnpm inference-tei-import +``` + +### inference-tgi-import.ts + +Fetches TGI specs and generates JSON schema for input, output and stream_output of text-generation and chat-completion tasks. +See https://huggingface.github.io/text-generation-inference/ for more details. + +This script is run by a cron job once a day and helps getting `@huggingface.js/tasks` up to date with TGI updates. + +To update the specs manually, run: + +``` +pnpm inference-tgi-import +``` diff --git a/packages/tasks-gen/package.json b/packages/tasks-gen/package.json new file mode 100644 index 000000000..23ad43cfc --- /dev/null +++ b/packages/tasks-gen/package.json @@ -0,0 +1,31 @@ +{ + "name": "@huggingface/tasks-gen", + "packageManager": "pnpm@8.10.5", + "version": "0.13.1", + "description": "Utilities to generate files for @huggingface/tasks, and avoid unnecessary deps in the published package", + "repository": "https://github.com/huggingface/huggingface.js.git", + "private": true, + "scripts": { + "lint": "eslint --quiet --fix --ext .cjs,.ts .", + "lint:check": "eslint --ext .cjs,.ts .", + "format": "prettier --write .", + "format:check": "prettier --check .", + "check": "tsc", + "generate-snippets-fixtures": "tsx scripts/generate-snippets-fixtures.ts", + "inference-codegen": "tsx scripts/inference-codegen.ts && prettier --write ../tasks/src/tasks/*/inference.ts", + "inference-tgi-import": "tsx scripts/inference-tgi-import.ts && prettier --write ../tasks/src/tasks/text-generation/spec/*.json && prettier --write ../tasks/src/tasks/chat-completion/spec/*.json", + "inference-tei-import": "tsx scripts/inference-tei-import.ts && prettier --write ../tasks/src/tasks/feature-extraction/spec/*.json", + "test": "vitest run" + }, + "type": "module", + "author": "Hugging Face", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.11.5", + "quicktype-core": "https://github.com/huggingface/quicktype/raw/pack-18.0.17/packages/quicktype-core/quicktype-core-18.0.17.tgz", + "type-fest": "^3.13.1" + }, + "dependencies": { + "@huggingface/tasks": "workspace:^" + } +} diff --git a/packages/tasks-gen/pnpm-lock.yaml b/packages/tasks-gen/pnpm-lock.yaml new file mode 100644 index 000000000..f754b702e --- /dev/null +++ b/packages/tasks-gen/pnpm-lock.yaml @@ -0,0 +1,228 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@huggingface/tasks': + specifier: workspace:^ + version: link:../tasks + +devDependencies: + '@types/node': + specifier: ^20.11.5 + version: 20.17.6 + quicktype-core: + specifier: https://github.com/huggingface/quicktype/raw/pack-18.0.17/packages/quicktype-core/quicktype-core-18.0.17.tgz + version: '@github.com/huggingface/quicktype/raw/pack-18.0.17/packages/quicktype-core/quicktype-core-18.0.17.tgz' + type-fest: + specifier: ^3.13.1 + version: 3.13.1 + +packages: + + /@glideapps/ts-necessities@2.1.3: + resolution: {integrity: sha512-q9U8v/n9qbkd2zDYjuX3qtlbl+OIyI9zF+zQhZjfYOE9VMDH7tfcUSJ9p0lXoY3lxmGFne09yi4iiNeQUwV7AA==} + dev: true + + /@types/node@20.17.6: + resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==} + dependencies: + undici-types: 6.19.8 + dev: true + + /@types/urijs@1.19.25: + resolution: {integrity: sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==} + dev: true + + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /browser-or-node@2.1.1: + resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} + dev: true + + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /collection-utils@1.0.1: + resolution: {integrity: sha512-LA2YTIlR7biSpXkKYwwuzGjwL5rjWEZVOSnvdUc7gObvWe4WkjxOpfrdhoP7Hs09YWDVfg0Mal9BpAqLfVEzQg==} + dev: true + + /cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: true + + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: true + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + dev: true + + /js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: true + + /pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + dev: true + + /pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + dev: true + + /pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + dev: true + + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: true + + /readable-stream@4.4.2: + resolution: {integrity: sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + dev: true + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: true + + /type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + dev: true + + /undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + dev: true + + /unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + dev: true + + /unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + dev: true + + /urijs@1.19.11: + resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} + dev: true + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: true + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: true + + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + dev: true + + /yaml@2.6.1: + resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} + engines: {node: '>= 14'} + hasBin: true + dev: true + + '@github.com/huggingface/quicktype/raw/pack-18.0.17/packages/quicktype-core/quicktype-core-18.0.17.tgz': + resolution: {tarball: https://github.com/huggingface/quicktype/raw/pack-18.0.17/packages/quicktype-core/quicktype-core-18.0.17.tgz} + name: quicktype-core + version: 18.0.17 + dependencies: + '@glideapps/ts-necessities': 2.1.3 + '@types/urijs': 1.19.25 + browser-or-node: 2.1.1 + collection-utils: 1.0.1 + cross-fetch: 4.0.0 + is-url: 1.2.4 + js-base64: 3.7.7 + lodash: 4.17.21 + pako: 1.0.11 + pluralize: 8.0.0 + readable-stream: 4.4.2 + unicode-properties: 1.4.1 + urijs: 1.19.11 + wordwrap: 1.0.0 + yaml: 2.6.1 + transitivePeerDependencies: + - encoding + dev: true diff --git a/packages/tasks-gen/scripts/generate-snippets-fixtures.ts b/packages/tasks-gen/scripts/generate-snippets-fixtures.ts new file mode 100644 index 000000000..1107d78a5 --- /dev/null +++ b/packages/tasks-gen/scripts/generate-snippets-fixtures.ts @@ -0,0 +1,178 @@ +/* + * Generates Inference API snippets using @huggingface/tasks snippets. + * + * If used in test mode ("pnpm test"), it compares the generated snippets with the expected ones. + * If used in generation mode ("pnpm generate-snippets-fixtures"), it generates the expected snippets. + * + * Expected snippets are saved under ./snippets-fixtures and are meant to be versioned on GitHub. + * Each snippet is saved in a separate file placed under "./{test-name}/{index}.{client}.{language}": + * - test-name: the name of the test (e.g. "text-to-image", "conversational-llm", etc.) + * - index: the order of the snippet in the array of snippets (0 if not an array) + * - client: the client name (e.g. "requests", "huggingface_hub", "openai", etc.). Default to "default" if client is not specified. + * - language: the language of the snippet (e.g. "sh", "js", "py", etc.) + * + * Example: + * ./packages/tasks-gen/snippets-fixtures/text-to-image/0.huggingface_hub.py + */ + +import { existsSync as pathExists } from "node:fs"; +import * as fs from "node:fs/promises"; +import * as path from "node:path/posix"; + +import type { InferenceSnippet } from "@huggingface/tasks"; +import { snippets } from "@huggingface/tasks"; + +type LANGUAGE = "sh" | "js" | "py"; + +const TEST_CASES: { + testName: string; + model: snippets.ModelDataMinimal; + languages: LANGUAGE[]; + opts?: Record; +}[] = [ + { + testName: "conversational-llm-non-stream", + model: { + id: "meta-llama/Llama-3.1-8B-Instruct", + pipeline_tag: "text-generation", + tags: ["conversational"], + inference: "", + }, + languages: ["sh", "js", "py"], + opts: { streaming: false }, + }, + { + testName: "conversational-llm-stream", + model: { + id: "meta-llama/Llama-3.1-8B-Instruct", + pipeline_tag: "text-generation", + tags: ["conversational"], + inference: "", + }, + languages: ["sh", "js", "py"], + opts: { streaming: true }, + }, + { + testName: "conversational-vlm-non-stream", + model: { + id: "meta-llama/Llama-3.2-11B-Vision-Instruct", + pipeline_tag: "image-text-to-text", + tags: ["conversational"], + inference: "", + }, + languages: ["sh", "js", "py"], + opts: { streaming: false }, + }, + { + testName: "conversational-vlm-stream", + model: { + id: "meta-llama/Llama-3.2-11B-Vision-Instruct", + pipeline_tag: "image-text-to-text", + tags: ["conversational"], + inference: "", + }, + languages: ["sh", "js", "py"], + opts: { streaming: true }, + }, + { + testName: "text-to-image", + model: { + id: "black-forest-labs/FLUX.1-schnell", + pipeline_tag: "text-to-image", + tags: [], + inference: "", + }, + languages: ["sh", "js", "py"], + }, +] as const; + +const GET_SNIPPET_FN = { + sh: snippets.curl.getCurlInferenceSnippet, + js: snippets.js.getJsInferenceSnippet, + py: snippets.python.getPythonInferenceSnippet, +} as const; + +const rootDirFinder = (): string => { + let currentPath = path.normalize(import.meta.url).replace("file:", ""); + + while (currentPath !== "/") { + if (pathExists(path.join(currentPath, "package.json"))) { + return currentPath; + } + + currentPath = path.normalize(path.join(currentPath, "..")); + } + + return "/"; +}; + +function getFixtureFolder(testName: string): string { + return path.join(rootDirFinder(), "snippets-fixtures", testName); +} + +function generateInferenceSnippet( + model: snippets.ModelDataMinimal, + language: LANGUAGE, + opts?: Record +): InferenceSnippet[] { + const generatedSnippets = GET_SNIPPET_FN[language](model, "api_token", opts); + return Array.isArray(generatedSnippets) ? generatedSnippets : [generatedSnippets]; +} + +async function getExpectedInferenceSnippet(testName: string, language: LANGUAGE): Promise { + const fixtureFolder = getFixtureFolder(testName); + const files = await fs.readdir(fixtureFolder); + + const expectedSnippets: InferenceSnippet[] = []; + for (const file of files.filter((file) => file.endsWith("." + language)).sort()) { + const client = path.basename(file).split(".").slice(1, -1).join("."); // e.g. '0.huggingface.js.js' => "huggingface.js" + const content = await fs.readFile(path.join(fixtureFolder, file), { encoding: "utf-8" }); + expectedSnippets.push(client === "default" ? { content } : { client, content }); + } + return expectedSnippets; +} + +async function saveExpectedInferenceSnippet(testName: string, language: LANGUAGE, snippets: InferenceSnippet[]) { + const fixtureFolder = getFixtureFolder(testName); + await fs.mkdir(fixtureFolder, { recursive: true }); + + for (const [index, snippet] of snippets.entries()) { + const file = path.join(fixtureFolder, `${index}.${snippet.client ?? "default"}.${language}`); + await fs.writeFile(file, snippet.content); + } +} + +if (import.meta.vitest) { + // Run test if in test mode + const { describe, expect, it } = import.meta.vitest; + + describe("inference API snippets", () => { + TEST_CASES.forEach(({ testName, model, languages, opts }) => { + describe(testName, () => { + languages.forEach((language) => { + it(language, async () => { + const generatedSnippets = generateInferenceSnippet(model, language, opts); + const expectedSnippets = await getExpectedInferenceSnippet(testName, language); + expect(generatedSnippets).toEqual(expectedSnippets); + }); + }); + }); + }); + }); +} else { + // Otherwise, generate the fixtures + console.log("✨ Re-generating snippets"); + console.debug(" 🚜 Removing existing fixtures..."); + await fs.rm(path.join(rootDirFinder(), "snippets-fixtures"), { recursive: true, force: true }); + + console.debug(" 🏭 Generating new fixtures..."); + TEST_CASES.forEach(({ testName, model, languages, opts }) => { + console.debug(` ${testName} (${languages.join(", ")})`); + languages.forEach(async (language) => { + const generatedSnippets = generateInferenceSnippet(model, language, opts); + await saveExpectedInferenceSnippet(testName, language, generatedSnippets); + }); + }); + console.log("✅ All done!"); + console.log("👉 Please check the generated fixtures before committing them."); +} diff --git a/packages/tasks/scripts/inference-codegen.ts b/packages/tasks-gen/scripts/inference-codegen.ts similarity index 98% rename from packages/tasks/scripts/inference-codegen.ts rename to packages/tasks-gen/scripts/inference-codegen.ts index 68d071e57..2b5375119 100644 --- a/packages/tasks/scripts/inference-codegen.ts +++ b/packages/tasks-gen/scripts/inference-codegen.ts @@ -22,8 +22,6 @@ const PYTHON_HEADER_FILE = ` # - specs: https://github.com/huggingface/huggingface.js/tree/main/packages/tasks/src/tasks. `; -const PYTHON_DIR = "./.python_generated"; - const rootDirFinder = function (): string { let currentPath = path.normalize(import.meta.url); @@ -172,8 +170,10 @@ async function postProcessOutput(path2generated: string, outputSpec: Record entry.isDirectory()) diff --git a/packages/tasks/scripts/inference-tei-import.ts b/packages/tasks-gen/scripts/inference-tei-import.ts similarity index 98% rename from packages/tasks/scripts/inference-tei-import.ts rename to packages/tasks-gen/scripts/inference-tei-import.ts index 2765294a7..c7af79547 100644 --- a/packages/tasks/scripts/inference-tei-import.ts +++ b/packages/tasks-gen/scripts/inference-tei-import.ts @@ -24,7 +24,7 @@ const rootDirFinder = function (): string { return "/"; }; -const rootDir = rootDirFinder(); +const rootDir = path.join(rootDirFinder(), "..", "tasks"); const tasksDir = path.join(rootDir, "src", "tasks"); function toCamelCase(str: string, joiner = "") { diff --git a/packages/tasks/scripts/inference-tgi-import.ts b/packages/tasks-gen/scripts/inference-tgi-import.ts similarity index 98% rename from packages/tasks/scripts/inference-tgi-import.ts rename to packages/tasks-gen/scripts/inference-tgi-import.ts index 852d1dc53..27060fd82 100644 --- a/packages/tasks/scripts/inference-tgi-import.ts +++ b/packages/tasks-gen/scripts/inference-tgi-import.ts @@ -24,7 +24,7 @@ const rootDirFinder = function (): string { return "/"; }; -const rootDir = rootDirFinder(); +const rootDir = path.join(rootDirFinder(), "..", "tasks"); const tasksDir = path.join(rootDir, "src", "tasks"); function toCamelCase(str: string, joiner = "") { diff --git a/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/0.default.sh b/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/0.default.sh new file mode 100644 index 000000000..6f9cf0d70 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/0.default.sh @@ -0,0 +1,14 @@ +curl 'https://api-inference.huggingface.co/models/meta-llama/Llama-3.1-8B-Instruct/v1/chat/completions' \ +-H 'Authorization: Bearer api_token' \ +-H 'Content-Type: application/json' \ +--data '{ + "model": "meta-llama/Llama-3.1-8B-Instruct", + "messages": [ + { + "role": "user", + "content": "What is the capital of France?" + } + ], + "max_tokens": 500, + "stream": false +}' \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/0.huggingface.js.js b/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/0.huggingface.js.js new file mode 100644 index 000000000..c9243e179 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/0.huggingface.js.js @@ -0,0 +1,16 @@ +import { HfInference } from "@huggingface/inference"; + +const client = new HfInference("api_token"); + +const chatCompletion = await client.chatCompletion({ + model: "meta-llama/Llama-3.1-8B-Instruct", + messages: [ + { + role: "user", + content: "What is the capital of France?" + } + ], + max_tokens: 500 +}); + +console.log(chatCompletion.choices[0].message); \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/0.huggingface_hub.py b/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/0.huggingface_hub.py new file mode 100644 index 000000000..e60e63114 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/0.huggingface_hub.py @@ -0,0 +1,18 @@ +from huggingface_hub import InferenceClient + +client = InferenceClient(api_key="api_token") + +messages = [ + { + "role": "user", + "content": "What is the capital of France?" + } +] + +completion = client.chat.completions.create( + model="meta-llama/Llama-3.1-8B-Instruct", + messages=messages, + max_tokens=500 +) + +print(completion.choices[0].message) \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/1.openai.js b/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/1.openai.js new file mode 100644 index 000000000..ddccf8d50 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/1.openai.js @@ -0,0 +1,19 @@ +import { OpenAI } from "openai"; + +const client = new OpenAI({ + baseURL: "https://api-inference.huggingface.co/v1/", + apiKey: "api_token" +}); + +const chatCompletion = await client.chat.completions.create({ + model: "meta-llama/Llama-3.1-8B-Instruct", + messages: [ + { + role: "user", + content: "What is the capital of France?" + } + ], + max_tokens: 500 +}); + +console.log(chatCompletion.choices[0].message); \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/1.openai.py b/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/1.openai.py new file mode 100644 index 000000000..e6f4b1b47 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-llm-non-stream/1.openai.py @@ -0,0 +1,21 @@ +from openai import OpenAI + +client = OpenAI( + base_url="https://api-inference.huggingface.co/v1/", + api_key="api_token" +) + +messages = [ + { + "role": "user", + "content": "What is the capital of France?" + } +] + +completion = client.chat.completions.create( + model="meta-llama/Llama-3.1-8B-Instruct", + messages=messages, + max_tokens=500 +) + +print(completion.choices[0].message) \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/0.default.sh b/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/0.default.sh new file mode 100644 index 000000000..42c9e2603 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/0.default.sh @@ -0,0 +1,14 @@ +curl 'https://api-inference.huggingface.co/models/meta-llama/Llama-3.1-8B-Instruct/v1/chat/completions' \ +-H 'Authorization: Bearer api_token' \ +-H 'Content-Type: application/json' \ +--data '{ + "model": "meta-llama/Llama-3.1-8B-Instruct", + "messages": [ + { + "role": "user", + "content": "What is the capital of France?" + } + ], + "max_tokens": 500, + "stream": true +}' \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/0.huggingface.js.js b/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/0.huggingface.js.js new file mode 100644 index 000000000..581e0a3e8 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/0.huggingface.js.js @@ -0,0 +1,24 @@ +import { HfInference } from "@huggingface/inference"; + +const client = new HfInference("api_token"); + +let out = ""; + +const stream = client.chatCompletionStream({ + model: "meta-llama/Llama-3.1-8B-Instruct", + messages: [ + { + role: "user", + content: "What is the capital of France?" + } + ], + max_tokens: 500 +}); + +for await (const chunk of stream) { + if (chunk.choices && chunk.choices.length > 0) { + const newContent = chunk.choices[0].delta.content; + out += newContent; + console.log(newContent); + } +} \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/0.huggingface_hub.py b/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/0.huggingface_hub.py new file mode 100644 index 000000000..38a5efcd6 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/0.huggingface_hub.py @@ -0,0 +1,20 @@ +from huggingface_hub import InferenceClient + +client = InferenceClient(api_key="api_token") + +messages = [ + { + "role": "user", + "content": "What is the capital of France?" + } +] + +stream = client.chat.completions.create( + model="meta-llama/Llama-3.1-8B-Instruct", + messages=messages, + max_tokens=500, + stream=True +) + +for chunk in stream: + print(chunk.choices[0].delta.content, end="") \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/1.openai.js b/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/1.openai.js new file mode 100644 index 000000000..ccb3cb15b --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/1.openai.js @@ -0,0 +1,28 @@ +import { OpenAI } from "openai"; + +const client = new OpenAI({ + baseURL: "https://api-inference.huggingface.co/v1/", + apiKey: "api_token" +}); + +let out = ""; + +const stream = await client.chat.completions.create({ + model: "meta-llama/Llama-3.1-8B-Instruct", + messages: [ + { + role: "user", + content: "What is the capital of France?" + } + ], + max_tokens: 500, + stream: true, +}); + +for await (const chunk of stream) { + if (chunk.choices && chunk.choices.length > 0) { + const newContent = chunk.choices[0].delta.content; + out += newContent; + console.log(newContent); + } +} \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/1.openai.py b/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/1.openai.py new file mode 100644 index 000000000..909a6e9e4 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-llm-stream/1.openai.py @@ -0,0 +1,23 @@ +from openai import OpenAI + +client = OpenAI( + base_url="https://api-inference.huggingface.co/v1/", + api_key="api_token" +) + +messages = [ + { + "role": "user", + "content": "What is the capital of France?" + } +] + +stream = client.chat.completions.create( + model="meta-llama/Llama-3.1-8B-Instruct", + messages=messages, + max_tokens=500, + stream=True +) + +for chunk in stream: + print(chunk.choices[0].delta.content, end="") \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/0.default.sh b/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/0.default.sh new file mode 100644 index 000000000..11df13c0b --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/0.default.sh @@ -0,0 +1,25 @@ +curl 'https://api-inference.huggingface.co/models/meta-llama/Llama-3.2-11B-Vision-Instruct/v1/chat/completions' \ +-H 'Authorization: Bearer api_token' \ +-H 'Content-Type: application/json' \ +--data '{ + "model": "meta-llama/Llama-3.2-11B-Vision-Instruct", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Describe this image in one sentence." + }, + { + "type": "image_url", + "image_url": { + "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" + } + } + ] + } + ], + "max_tokens": 500, + "stream": false +}' \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/0.huggingface.js.js b/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/0.huggingface.js.js new file mode 100644 index 000000000..b7e54db67 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/0.huggingface.js.js @@ -0,0 +1,27 @@ +import { HfInference } from "@huggingface/inference"; + +const client = new HfInference("api_token"); + +const chatCompletion = await client.chatCompletion({ + model: "meta-llama/Llama-3.2-11B-Vision-Instruct", + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: "Describe this image in one sentence." + }, + { + type: "image_url", + image_url: { + url: "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" + } + } + ] + } + ], + max_tokens: 500 +}); + +console.log(chatCompletion.choices[0].message); \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/0.huggingface_hub.py b/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/0.huggingface_hub.py new file mode 100644 index 000000000..82c238997 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/0.huggingface_hub.py @@ -0,0 +1,29 @@ +from huggingface_hub import InferenceClient + +client = InferenceClient(api_key="api_token") + +messages = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Describe this image in one sentence." + }, + { + "type": "image_url", + "image_url": { + "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" + } + } + ] + } +] + +completion = client.chat.completions.create( + model="meta-llama/Llama-3.2-11B-Vision-Instruct", + messages=messages, + max_tokens=500 +) + +print(completion.choices[0].message) \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/1.openai.js b/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/1.openai.js new file mode 100644 index 000000000..6badefd52 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/1.openai.js @@ -0,0 +1,30 @@ +import { OpenAI } from "openai"; + +const client = new OpenAI({ + baseURL: "https://api-inference.huggingface.co/v1/", + apiKey: "api_token" +}); + +const chatCompletion = await client.chat.completions.create({ + model: "meta-llama/Llama-3.2-11B-Vision-Instruct", + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: "Describe this image in one sentence." + }, + { + type: "image_url", + image_url: { + url: "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" + } + } + ] + } + ], + max_tokens: 500 +}); + +console.log(chatCompletion.choices[0].message); \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/1.openai.py b/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/1.openai.py new file mode 100644 index 000000000..7a68e8b2c --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-vlm-non-stream/1.openai.py @@ -0,0 +1,32 @@ +from openai import OpenAI + +client = OpenAI( + base_url="https://api-inference.huggingface.co/v1/", + api_key="api_token" +) + +messages = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Describe this image in one sentence." + }, + { + "type": "image_url", + "image_url": { + "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" + } + } + ] + } +] + +completion = client.chat.completions.create( + model="meta-llama/Llama-3.2-11B-Vision-Instruct", + messages=messages, + max_tokens=500 +) + +print(completion.choices[0].message) \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/0.default.sh b/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/0.default.sh new file mode 100644 index 000000000..5b60847e1 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/0.default.sh @@ -0,0 +1,25 @@ +curl 'https://api-inference.huggingface.co/models/meta-llama/Llama-3.2-11B-Vision-Instruct/v1/chat/completions' \ +-H 'Authorization: Bearer api_token' \ +-H 'Content-Type: application/json' \ +--data '{ + "model": "meta-llama/Llama-3.2-11B-Vision-Instruct", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Describe this image in one sentence." + }, + { + "type": "image_url", + "image_url": { + "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" + } + } + ] + } + ], + "max_tokens": 500, + "stream": true +}' \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/0.huggingface.js.js b/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/0.huggingface.js.js new file mode 100644 index 000000000..e91f14d81 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/0.huggingface.js.js @@ -0,0 +1,35 @@ +import { HfInference } from "@huggingface/inference"; + +const client = new HfInference("api_token"); + +let out = ""; + +const stream = client.chatCompletionStream({ + model: "meta-llama/Llama-3.2-11B-Vision-Instruct", + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: "Describe this image in one sentence." + }, + { + type: "image_url", + image_url: { + url: "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" + } + } + ] + } + ], + max_tokens: 500 +}); + +for await (const chunk of stream) { + if (chunk.choices && chunk.choices.length > 0) { + const newContent = chunk.choices[0].delta.content; + out += newContent; + console.log(newContent); + } +} \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/0.huggingface_hub.py b/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/0.huggingface_hub.py new file mode 100644 index 000000000..9eaf7a167 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/0.huggingface_hub.py @@ -0,0 +1,31 @@ +from huggingface_hub import InferenceClient + +client = InferenceClient(api_key="api_token") + +messages = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Describe this image in one sentence." + }, + { + "type": "image_url", + "image_url": { + "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" + } + } + ] + } +] + +stream = client.chat.completions.create( + model="meta-llama/Llama-3.2-11B-Vision-Instruct", + messages=messages, + max_tokens=500, + stream=True +) + +for chunk in stream: + print(chunk.choices[0].delta.content, end="") \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/1.openai.js b/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/1.openai.js new file mode 100644 index 000000000..59447faa0 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/1.openai.js @@ -0,0 +1,39 @@ +import { OpenAI } from "openai"; + +const client = new OpenAI({ + baseURL: "https://api-inference.huggingface.co/v1/", + apiKey: "api_token" +}); + +let out = ""; + +const stream = await client.chat.completions.create({ + model: "meta-llama/Llama-3.2-11B-Vision-Instruct", + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: "Describe this image in one sentence." + }, + { + type: "image_url", + image_url: { + url: "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" + } + } + ] + } + ], + max_tokens: 500, + stream: true, +}); + +for await (const chunk of stream) { + if (chunk.choices && chunk.choices.length > 0) { + const newContent = chunk.choices[0].delta.content; + out += newContent; + console.log(newContent); + } +} \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/1.openai.py b/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/1.openai.py new file mode 100644 index 000000000..a3fe75bcf --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/conversational-vlm-stream/1.openai.py @@ -0,0 +1,34 @@ +from openai import OpenAI + +client = OpenAI( + base_url="https://api-inference.huggingface.co/v1/", + api_key="api_token" +) + +messages = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Describe this image in one sentence." + }, + { + "type": "image_url", + "image_url": { + "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" + } + } + ] + } +] + +stream = client.chat.completions.create( + model="meta-llama/Llama-3.2-11B-Vision-Instruct", + messages=messages, + max_tokens=500, + stream=True +) + +for chunk in stream: + print(chunk.choices[0].delta.content, end="") \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/text-to-image/0.default.js b/packages/tasks-gen/snippets-fixtures/text-to-image/0.default.js new file mode 100644 index 000000000..b5ec56c29 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/text-to-image/0.default.js @@ -0,0 +1,18 @@ +async function query(data) { + const response = await fetch( + "https://api-inference.huggingface.co/models/black-forest-labs/FLUX.1-schnell", + { + headers: { + Authorization: "Bearer api_token", + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify(data), + } + ); + const result = await response.blob(); + return result; +} +query({"inputs": "Astronaut riding a horse"}).then((response) => { + // Use image +}); \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/text-to-image/0.default.sh b/packages/tasks-gen/snippets-fixtures/text-to-image/0.default.sh new file mode 100644 index 000000000..73f3c2d54 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/text-to-image/0.default.sh @@ -0,0 +1,5 @@ +curl https://api-inference.huggingface.co/models/black-forest-labs/FLUX.1-schnell \ + -X POST \ + -d '{"inputs": "Astronaut riding a horse"}' \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer api_token' \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/text-to-image/0.huggingface_hub.py b/packages/tasks-gen/snippets-fixtures/text-to-image/0.huggingface_hub.py new file mode 100644 index 000000000..a0914bc5e --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/text-to-image/0.huggingface_hub.py @@ -0,0 +1,5 @@ +from huggingface_hub import InferenceClient +client = InferenceClient("black-forest-labs/FLUX.1-schnell", token="api_token") + +# output is a PIL.Image object +image = client.text_to_image("Astronaut riding a horse") \ No newline at end of file diff --git a/packages/tasks-gen/snippets-fixtures/text-to-image/1.requests.py b/packages/tasks-gen/snippets-fixtures/text-to-image/1.requests.py new file mode 100644 index 000000000..e71ec1936 --- /dev/null +++ b/packages/tasks-gen/snippets-fixtures/text-to-image/1.requests.py @@ -0,0 +1,16 @@ +import requests + +API_URL = "https://api-inference.huggingface.co/models/black-forest-labs/FLUX.1-schnell" +headers = {"Authorization": "Bearer api_token"} + +def query(payload): + response = requests.post(API_URL, headers=headers, json=payload) + return response.content +image_bytes = query({ + "inputs": "Astronaut riding a horse", +}) + +# You can access the image with PIL.Image for example +import io +from PIL import Image +image = Image.open(io.BytesIO(image_bytes)) \ No newline at end of file diff --git a/packages/tasks-gen/tsconfig.json b/packages/tasks-gen/tsconfig.json new file mode 100644 index 000000000..b4441ee9b --- /dev/null +++ b/packages/tasks-gen/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "lib": ["ES2022", "DOM"], + "module": "NodeNext", + "target": "ESNext", + "moduleResolution": "nodenext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "skipLibCheck": true, + "noImplicitOverride": true, + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "types": ["vitest/importMeta"] + }, + "include": ["scripts"], + "exclude": ["dist"] +} diff --git a/packages/tasks-gen/vitest.config.ts b/packages/tasks-gen/vitest.config.ts new file mode 100644 index 000000000..3cd4d43fb --- /dev/null +++ b/packages/tasks-gen/vitest.config.ts @@ -0,0 +1,8 @@ +// vitest.config.ts +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + includeSource: ["scripts/generate-snippets-fixtures.ts"], + }, +}); diff --git a/packages/tasks/.prettierignore b/packages/tasks/.prettierignore index cac0c6949..09c2c8882 100644 --- a/packages/tasks/.prettierignore +++ b/packages/tasks/.prettierignore @@ -1,4 +1,5 @@ pnpm-lock.yaml # In order to avoid code samples to have tabs, they don't display well on npm README.md -dist \ No newline at end of file +dist +.tshy \ No newline at end of file diff --git a/packages/tasks/.tshy/build.json b/packages/tasks/.tshy/build.json new file mode 100644 index 000000000..aea1a9e93 --- /dev/null +++ b/packages/tasks/.tshy/build.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "../src", + "module": "nodenext", + "moduleResolution": "nodenext" + } +} diff --git a/packages/tasks/.tshy/commonjs.json b/packages/tasks/.tshy/commonjs.json new file mode 100644 index 000000000..7c9db50b6 --- /dev/null +++ b/packages/tasks/.tshy/commonjs.json @@ -0,0 +1,16 @@ +{ + "extends": "./build.json", + "include": [ + "../src/**/*.ts", + "../src/**/*.cts", + "../src/**/*.tsx", + "../src/**/*.json" + ], + "exclude": [ + "../src/**/*.mts", + "../src/package.json" + ], + "compilerOptions": { + "outDir": "../.tshy-build/commonjs" + } +} diff --git a/packages/tasks/.tshy/esm.json b/packages/tasks/.tshy/esm.json new file mode 100644 index 000000000..959294a84 --- /dev/null +++ b/packages/tasks/.tshy/esm.json @@ -0,0 +1,15 @@ +{ + "extends": "./build.json", + "include": [ + "../src/**/*.ts", + "../src/**/*.mts", + "../src/**/*.tsx", + "../src/**/*.json" + ], + "exclude": [ + "../src/package.json" + ], + "compilerOptions": { + "outDir": "../.tshy-build/esm" + } +} diff --git a/packages/tasks/package.json b/packages/tasks/package.json index baf2d918f..78bf63d21 100644 --- a/packages/tasks/package.json +++ b/packages/tasks/package.json @@ -1,38 +1,39 @@ { "name": "@huggingface/tasks", "packageManager": "pnpm@8.10.5", - "version": "0.12.26", + "version": "0.13.13", "description": "List of ML tasks for huggingface.co/tasks", "repository": "https://github.com/huggingface/huggingface.js.git", "publishConfig": { "access": "public" }, - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/src/index.d.ts", "exports": { + "./package.json": "./package.json", ".": { - "types": "./dist/src/index.d.ts", - "require": "./dist/index.cjs", - "import": "./dist/index.js" + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } } }, - "source": "src/index.ts", + "source": "./src/index.ts", "scripts": { "lint": "eslint --quiet --fix --ext .cjs,.ts .", "lint:check": "eslint --ext .cjs,.ts .", "format": "prettier --write .", "format:check": "prettier --check .", - "prepublishOnly": "pnpm run inference-codegen && git diff --name-only --exit-code src && pnpm run build", - "build": "tsup src/index.ts --format cjs,esm --clean && tsc --emitDeclarationOnly --declaration", - "watch:export": "tsup src/index.ts --format cjs,esm --watch", - "watch:types": "tsc --emitDeclarationOnly --declaration --watch", - "watch": "npm-run-all --parallel watch:export watch:types", + "prepublishOnly": "pnpm --filter tasks-gen inference-codegen && git diff --name-only --exit-code src && pnpm run build", + "build": "tshy", + "watch:cjs": "tsc --declaration --outdir dist/commonjs --module commonjs --watch", + "watch:esm": "tsc --declaration --outdir dist/esm --watch", + "watch": "npm-run-all --parallel watch:esm watch:cjs", "prepare": "pnpm run build", "check": "tsc", - "inference-codegen": "tsx scripts/inference-codegen.ts && prettier --write src/tasks/*/inference.ts", - "inference-tgi-import": "tsx scripts/inference-tgi-import.ts && prettier --write src/tasks/text-generation/spec/*.json && prettier --write src/tasks/chat-completion/spec/*.json", - "inference-tei-import": "tsx scripts/inference-tei-import.ts && prettier --write src/tasks/feature-extraction/spec/*.json" + "test": "vitest run" }, "type": "module", "files": [ @@ -47,12 +48,13 @@ ], "author": "Hugging Face", "license": "MIT", - "devDependencies": { - "@types/node": "^20.11.5", - "quicktype-core": "https://github.com/huggingface/quicktype/raw/pack-18.0.17/packages/quicktype-core/quicktype-core-18.0.17.tgz", - "type-fest": "^3.13.1" + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts" + } }, - "dependencies": { - "@huggingface/gguf": "workspace:^" - } + "main": "./dist/commonjs/index.js", + "types": "./dist/commonjs/index.d.ts", + "module": "./dist/esm/index.js" } diff --git a/packages/tasks/pnpm-lock.yaml b/packages/tasks/pnpm-lock.yaml index 2d5435a89..2b9f1883a 100644 --- a/packages/tasks/pnpm-lock.yaml +++ b/packages/tasks/pnpm-lock.yaml @@ -3,225 +3,3 @@ lockfileVersion: '6.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false - -dependencies: - '@huggingface/gguf': - specifier: workspace:^ - version: link:../gguf - -devDependencies: - '@types/node': - specifier: ^20.11.5 - version: 20.11.5 - quicktype-core: - specifier: https://github.com/huggingface/quicktype/raw/pack-18.0.17/packages/quicktype-core/quicktype-core-18.0.17.tgz - version: '@github.com/huggingface/quicktype/raw/pack-18.0.17/packages/quicktype-core/quicktype-core-18.0.17.tgz' - type-fest: - specifier: ^3.13.1 - version: 3.13.1 - -packages: - - /@glideapps/ts-necessities@2.1.3: - resolution: {integrity: sha512-q9U8v/n9qbkd2zDYjuX3qtlbl+OIyI9zF+zQhZjfYOE9VMDH7tfcUSJ9p0lXoY3lxmGFne09yi4iiNeQUwV7AA==} - dev: true - - /@types/node@20.11.5: - resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==} - dependencies: - undici-types: 5.26.5 - dev: true - - /@types/urijs@1.19.25: - resolution: {integrity: sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==} - dev: true - - /abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - dependencies: - event-target-shim: 5.0.1 - dev: true - - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true - - /browser-or-node@2.1.1: - resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} - dev: true - - /buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: true - - /collection-utils@1.0.1: - resolution: {integrity: sha512-LA2YTIlR7biSpXkKYwwuzGjwL5rjWEZVOSnvdUc7gObvWe4WkjxOpfrdhoP7Hs09YWDVfg0Mal9BpAqLfVEzQg==} - dev: true - - /cross-fetch@4.0.0: - resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - dev: true - - /event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - dev: true - - /events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - dev: true - - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: true - - /is-url@1.2.4: - resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} - dev: true - - /js-base64@3.7.6: - resolution: {integrity: sha512-NPrWuHFxFUknr1KqJRDgUQPexQF0uIJWjeT+2KjEePhitQxQEx5EJBG1lVn5/hc8aLycTpXrDOgPQ6Zq+EDiTA==} - dev: true - - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true - - /node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - dev: true - - /pako@0.2.9: - resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} - dev: true - - /pako@1.0.11: - resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - dev: true - - /pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - dev: true - - /process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - dev: true - - /readable-stream@4.4.2: - resolution: {integrity: sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - abort-controller: 3.0.0 - buffer: 6.0.3 - events: 3.3.0 - process: 0.11.10 - string_decoder: 1.3.0 - dev: true - - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: true - - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - dependencies: - safe-buffer: 5.2.1 - dev: true - - /tiny-inflate@1.0.3: - resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} - dev: true - - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: true - - /type-fest@3.13.1: - resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} - engines: {node: '>=14.16'} - dev: true - - /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true - - /unicode-properties@1.4.1: - resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} - dependencies: - base64-js: 1.5.1 - unicode-trie: 2.0.0 - dev: true - - /unicode-trie@2.0.0: - resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} - dependencies: - pako: 0.2.9 - tiny-inflate: 1.0.3 - dev: true - - /urijs@1.19.11: - resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} - dev: true - - /webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: true - - /whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - dev: true - - /wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - dev: true - - /yaml@2.3.4: - resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} - engines: {node: '>= 14'} - dev: true - - '@github.com/huggingface/quicktype/raw/pack-18.0.17/packages/quicktype-core/quicktype-core-18.0.17.tgz': - resolution: {tarball: https://github.com/huggingface/quicktype/raw/pack-18.0.17/packages/quicktype-core/quicktype-core-18.0.17.tgz} - name: quicktype-core - version: 18.0.17 - dependencies: - '@glideapps/ts-necessities': 2.1.3 - '@types/urijs': 1.19.25 - browser-or-node: 2.1.1 - collection-utils: 1.0.1 - cross-fetch: 4.0.0 - is-url: 1.2.4 - js-base64: 3.7.6 - lodash: 4.17.21 - pako: 1.0.11 - pluralize: 8.0.0 - readable-stream: 4.4.2 - unicode-properties: 1.4.1 - urijs: 1.19.11 - wordwrap: 1.0.0 - yaml: 2.3.4 - transitivePeerDependencies: - - encoding - dev: true diff --git a/packages/tasks/src/default-widget-inputs.ts b/packages/tasks/src/default-widget-inputs.ts index b0f96ff40..220911e69 100644 --- a/packages/tasks/src/default-widget-inputs.ts +++ b/packages/tasks/src/default-widget-inputs.ts @@ -1,5 +1,5 @@ -import type { WidgetExample } from "./widget-example"; -import type { WidgetType } from "./pipelines"; +import type { WidgetExample } from "./widget-example.js"; +import type { WidgetType } from "./pipelines.js"; type LanguageCode = string; @@ -69,7 +69,7 @@ const MAPPING_EN: PerLanguageMapping = new Map([ "zero-shot-classification", [ { - text: "I have a problem with my iphone that needs to be resolved asap!!", + text: "I have a problem with my iphone that needs to be resolved asap!", candidate_labels: "urgent, not urgent, phone, tablet, computer", multi_class: true, }, @@ -95,24 +95,18 @@ const MAPPING_EN: PerLanguageMapping = new Map([ [ "conversational", [ - `Hey my name is Julien! How are you?`, - `Hey my name is Thomas! How are you?`, - `Hey my name is Mariama! How are you?`, + `Hi, what can you help me with?`, + `Hey, let's have a conversation!`, + `Hello there!`, `Hey my name is Clara! How are you?`, - `Hey my name is Julien! How are you?`, - `Hi.`, ], ], [ "text-generation", [ `My name is Julien and I like to`, - `My name is Thomas and my main`, - `My name is Mariama, my favorite`, - `My name is Clara and I am`, - `My name is Lewis and I like to`, - `My name is Merve and my favorite`, - `My name is Teven and I am`, + `I like traveling by train because`, + `Paris is an amazing place to visit,`, `Once upon a time,`, ], ], diff --git a/packages/tasks/src/gguf.ts b/packages/tasks/src/gguf.ts new file mode 100644 index 000000000..51ca8e841 --- /dev/null +++ b/packages/tasks/src/gguf.ts @@ -0,0 +1,40 @@ +export enum GGMLQuantizationType { + F32 = 0, + F16 = 1, + Q4_0 = 2, + Q4_1 = 3, + Q5_0 = 6, + Q5_1 = 7, + Q8_0 = 8, + Q8_1 = 9, + Q2_K = 10, + Q3_K = 11, + Q4_K = 12, + Q5_K = 13, + Q6_K = 14, + Q8_K = 15, + IQ2_XXS = 16, + IQ2_XS = 17, + IQ3_XXS = 18, + IQ1_S = 19, + IQ4_NL = 20, + IQ3_S = 21, + IQ2_S = 22, + IQ4_XS = 23, + I8 = 24, + I16 = 25, + I32 = 26, + I64 = 27, + F64 = 28, + IQ1_M = 29, + BF16 = 30, +} + +const ggufQuants = Object.values(GGMLQuantizationType).filter((v): v is string => typeof v === "string"); +export const GGUF_QUANT_RE = new RegExp(`(?${ggufQuants.join("|")})` + "(_(?[A-Z]+))?"); +export const GGUF_QUANT_RE_GLOBAL = new RegExp(GGUF_QUANT_RE, "g"); + +export function parseGGUFQuantLabel(fname: string): string | undefined { + const quantLabel = fname.toUpperCase().match(GGUF_QUANT_RE_GLOBAL)?.at(-1); // if there is multiple quant substrings in a name, we prefer the last one + return quantLabel; +} diff --git a/packages/tasks/src/hardware.ts b/packages/tasks/src/hardware.ts index 2bae4bdb6..3e8e16c29 100644 --- a/packages/tasks/src/hardware.ts +++ b/packages/tasks/src/hardware.ts @@ -76,6 +76,10 @@ export const SKUS = { tflops: 19.2, memory: [16], }, + "RTX A2000": { + tflops: 7.987, + memory: [8, 12], + }, A100: { tflops: 77.97, memory: [80, 40], @@ -303,6 +307,24 @@ export const SKUS = { memory: [16], }, }, + INTEL: { + "Arc A750": { + tflops: 34.41, + memory: [8], + }, + "Arc A770": { + tflops: 39.32, + memory: [8, 16], + }, + "Arc B570": { + tflops: 23.04, + memory: [10], + }, + "Arc B580": { + tflops: 27.34, + memory: [12], + }, + }, QUALCOMM: { "Snapdragon X Elite X1E-00-1DE": { tflops: 4.6, @@ -452,7 +474,7 @@ export const SKUS = { memory: [8, 16, 24], }, "Apple M2 Pro": { - tflops: 13.6, + tflops: 6.8, memory: [16, 24, 32], }, "Apple M2 Max": { @@ -464,17 +486,29 @@ export const SKUS = { memory: [64, 96, 128, 192], }, "Apple M3": { - tflops: 2.84, + tflops: 4.1, memory: [8, 16, 24], }, "Apple M3 Pro": { - tflops: 14, + tflops: 7.4, memory: [18, 36], }, "Apple M3 Max": { tflops: 14.2, memory: [36, 48, 64, 96, 128], }, + "Apple M4": { + tflops: 4.6, + memory: [16, 24, 32], + }, + "Apple M4 Pro": { + tflops: 9.2, + memory: [24, 48, 64], + }, + "Apple M4 Max": { + tflops: 18.4, + memory: [36, 48, 64, 128], + }, }, }, } satisfies Record>>; diff --git a/packages/tasks/src/index.ts b/packages/tasks/src/index.ts index cefebfaea..c350fecd2 100644 --- a/packages/tasks/src/index.ts +++ b/packages/tasks/src/index.ts @@ -1,7 +1,7 @@ -export { LIBRARY_TASK_MAPPING } from "./library-to-tasks"; -export { MAPPING_DEFAULT_WIDGET } from "./default-widget-inputs"; -export type { TaskData, TaskDemo, TaskDemoEntry, ExampleRepo } from "./tasks"; -export * from "./tasks"; +export { LIBRARY_TASK_MAPPING } from "./library-to-tasks.js"; +export { MAPPING_DEFAULT_WIDGET } from "./default-widget-inputs.js"; +export type { TaskData, TaskDemo, TaskDemoEntry, ExampleRepo } from "./tasks/index.js"; +export * from "./tasks/index.js"; export { PIPELINE_DATA, PIPELINE_TYPES, @@ -13,11 +13,15 @@ export { MODALITY_LABELS, SUBTASK_TYPES, PIPELINE_TYPES_SET, -} from "./pipelines"; -export { ALL_DISPLAY_MODEL_LIBRARY_KEYS, ALL_MODEL_LIBRARY_KEYS, MODEL_LIBRARIES_UI_ELEMENTS } from "./model-libraries"; -export type { LibraryUiElement, ModelLibraryKey } from "./model-libraries"; -export type { ModelData, TransformersInfo } from "./model-data"; -export type { AddedToken, SpecialTokensMap, TokenizerConfig } from "./tokenizer-data"; +} from "./pipelines.js"; +export { + ALL_DISPLAY_MODEL_LIBRARY_KEYS, + ALL_MODEL_LIBRARY_KEYS, + MODEL_LIBRARIES_UI_ELEMENTS, +} from "./model-libraries.js"; +export type { LibraryUiElement, ModelLibraryKey } from "./model-libraries.js"; +export type { ModelData, TransformersInfo } from "./model-data.js"; +export type { AddedToken, SpecialTokensMap, TokenizerConfig } from "./tokenizer-data.js"; export type { WidgetExample, WidgetExampleAttribute, @@ -38,16 +42,19 @@ export type { WidgetExampleOutputLabels, WidgetExampleOutputAnswerScore, WidgetExampleOutputText, -} from "./widget-example"; -export { SPECIAL_TOKENS_ATTRIBUTES } from "./tokenizer-data"; +} from "./widget-example.js"; +export { SPECIAL_TOKENS_ATTRIBUTES } from "./tokenizer-data.js"; + +import * as snippets from "./snippets/index.js"; +export * from "./gguf.js"; -import * as snippets from "./snippets"; export { snippets }; +export type { InferenceSnippet } from "./snippets/index.js"; -export { SKUS, DEFAULT_MEMORY_OPTIONS } from "./hardware"; -export type { HardwareSpec, SkuType } from "./hardware"; -export { LOCAL_APPS } from "./local-apps"; -export type { LocalApp, LocalAppKey, LocalAppSnippet } from "./local-apps"; +export { SKUS, DEFAULT_MEMORY_OPTIONS } from "./hardware.js"; +export type { HardwareSpec, SkuType } from "./hardware.js"; +export { LOCAL_APPS } from "./local-apps.js"; +export type { LocalApp, LocalAppKey, LocalAppSnippet } from "./local-apps.js"; -export { DATASET_LIBRARIES_UI_ELEMENTS } from "./dataset-libraries"; -export type { DatasetLibraryUiElement, DatasetLibraryKey } from "./dataset-libraries"; +export { DATASET_LIBRARIES_UI_ELEMENTS } from "./dataset-libraries.js"; +export type { DatasetLibraryUiElement, DatasetLibraryKey } from "./dataset-libraries.js"; diff --git a/packages/tasks/src/library-to-tasks.ts b/packages/tasks/src/library-to-tasks.ts index c8411fbaa..e3f0f5b60 100644 --- a/packages/tasks/src/library-to-tasks.ts +++ b/packages/tasks/src/library-to-tasks.ts @@ -1,5 +1,5 @@ -import type { ModelLibraryKey } from "./model-libraries"; -import type { PipelineType } from "./pipelines"; +import type { ModelLibraryKey } from "./model-libraries.js"; +import type { PipelineType } from "./pipelines.js"; /** * Mapping from library name to its supported tasks. diff --git a/packages/tasks/src/local-apps.spec.ts b/packages/tasks/src/local-apps.spec.ts new file mode 100644 index 000000000..23806f668 --- /dev/null +++ b/packages/tasks/src/local-apps.spec.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from "vitest"; +import { LOCAL_APPS } from "./local-apps.js"; +import type { ModelData } from "./model-data.js"; + +describe("local-apps", () => { + it("llama.cpp conversational", async () => { + const { snippet: snippetFunc } = LOCAL_APPS["llama.cpp"]; + const model: ModelData = { + id: "bartowski/Llama-3.2-3B-Instruct-GGUF", + tags: ["conversational"], + inference: "", + }; + const snippet = snippetFunc(model); + + expect(snippet[0].content).toEqual(`# Load and run the model: +llama-cli \\ + --hf-repo "bartowski/Llama-3.2-3B-Instruct-GGUF" \\ + --hf-file {{GGUF_FILE}} \\ + -p "You are a helpful assistant" \\ + --conversation`); + }); + + it("llama.cpp non-conversational", async () => { + const { snippet: snippetFunc } = LOCAL_APPS["llama.cpp"]; + const model: ModelData = { + id: "mlabonne/gemma-2b-GGUF", + tags: [], + inference: "", + }; + const snippet = snippetFunc(model); + + expect(snippet[0].content).toEqual(`# Load and run the model: +llama-cli \\ + --hf-repo "mlabonne/gemma-2b-GGUF" \\ + --hf-file {{GGUF_FILE}} \\ + -p "Once upon a time,"`); + }); + + it("vLLM conversational llm", async () => { + const { snippet: snippetFunc } = LOCAL_APPS["vllm"]; + const model: ModelData = { + id: "meta-llama/Llama-3.2-3B-Instruct", + pipeline_tag: "text-generation", + tags: ["conversational"], + inference: "", + }; + const snippet = snippetFunc(model); + + expect((snippet[0].content as string[]).join("\n")).toEqual(`# Load and run the model: +vllm serve "meta-llama/Llama-3.2-3B-Instruct" +# Call the server using curl: +curl -X POST "http://localhost:8000/v1/chat/completions" \\ + -H "Content-Type: application/json" \\ + --data '{ + "model": "meta-llama/Llama-3.2-3B-Instruct", + "messages": [ + { + "role": "user", + "content": "What is the capital of France?" + } + ] + }'`); + }); + + it("vLLM non-conversational llm", async () => { + const { snippet: snippetFunc } = LOCAL_APPS["vllm"]; + const model: ModelData = { + id: "meta-llama/Llama-3.2-3B", + tags: [""], + inference: "", + }; + const snippet = snippetFunc(model); + + expect((snippet[0].content as string[]).join("\n")).toEqual(`# Load and run the model: +vllm serve "meta-llama/Llama-3.2-3B" +# Call the server using curl: +curl -X POST "http://localhost:8000/v1/completions" \\ + -H "Content-Type: application/json" \\ + --data '{ + "model": "meta-llama/Llama-3.2-3B", + "prompt": "Once upon a time,", + "max_tokens": 512, + "temperature": 0.5 + }'`); + }); + + it("vLLM conversational vlm", async () => { + const { snippet: snippetFunc } = LOCAL_APPS["vllm"]; + const model: ModelData = { + id: "meta-llama/Llama-3.2-11B-Vision-Instruct", + pipeline_tag: "image-text-to-text", + tags: ["conversational"], + inference: "", + }; + const snippet = snippetFunc(model); + + expect((snippet[0].content as string[]).join("\n")).toEqual(`# Load and run the model: +vllm serve "meta-llama/Llama-3.2-11B-Vision-Instruct" +# Call the server using curl: +curl -X POST "http://localhost:8000/v1/chat/completions" \\ + -H "Content-Type: application/json" \\ + --data '{ + "model": "meta-llama/Llama-3.2-11B-Vision-Instruct", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Describe this image in one sentence." + }, + { + "type": "image_url", + "image_url": { + "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" + } + } + ] + } + ] + }'`); + }); +}); diff --git a/packages/tasks/src/local-apps.ts b/packages/tasks/src/local-apps.ts index 3fe1cbfac..0f2695c13 100644 --- a/packages/tasks/src/local-apps.ts +++ b/packages/tasks/src/local-apps.ts @@ -1,6 +1,9 @@ -import type { ModelData } from "./model-data"; -import type { PipelineType } from "./pipelines"; -import { parseGGUFQuantLabel } from "@huggingface/gguf"; +import { parseGGUFQuantLabel } from "./gguf.js"; +import type { ModelData } from "./model-data.js"; +import type { PipelineType } from "./pipelines.js"; +import { stringifyMessages } from "./snippets/common.js"; +import { getModelInputSnippet } from "./snippets/inputs.js"; +import type { ChatCompletionInputMessage } from "./tasks/index.js"; export interface LocalAppSnippet { /** @@ -92,15 +95,20 @@ function isMlxModel(model: ModelData) { } const snippetLlamacpp = (model: ModelData, filepath?: string): LocalAppSnippet[] => { - const command = (binary: string) => - [ + const command = (binary: string) => { + const snippet = [ "# Load and run the model:", `${binary} \\`, ` --hf-repo "${model.id}" \\`, ` --hf-file ${filepath ?? "{{GGUF_FILE}}"} \\`, - ' -p "You are a helpful assistant" \\', - " --conversation", - ].join("\n"); + ` -p "${model.tags.includes("conversational") ? "You are a helpful assistant" : "Once upon a time,"}"`, + ]; + if (model.tags.includes("conversational")) { + snippet[snippet.length - 1] += " \\"; + snippet.push(" --conversation"); + } + return snippet.join("\n"); + }; return [ { title: "Install from brew", @@ -121,9 +129,10 @@ const snippetLlamacpp = (model: ModelData, filepath?: string): LocalAppSnippet[] setup: [ "git clone https://github.com/ggerganov/llama.cpp.git", "cd llama.cpp", - "LLAMA_CURL=1 make llama-cli", + "cmake -B build -DLLAMA_CURL=ON", + "cmake --build build -j --target llama-cli", ].join("\n"), - content: command("./llama-cli"), + content: command("./build/bin/llama-cli"), }, ]; }; @@ -178,22 +187,33 @@ const snippetLocalAI = (model: ModelData, filepath?: string): LocalAppSnippet[] }; const snippetVllm = (model: ModelData): LocalAppSnippet[] => { - const runCommand = [ - "# Call the server using curl:", - `curl -X POST "http://localhost:8000/v1/chat/completions" \\`, - ` -H "Content-Type: application/json" \\`, - ` --data '{`, - ` "model": "${model.id}",`, - ` "messages": [`, - ` {"role": "user", "content": "Hello!"}`, - ` ]`, - ` }'`, - ]; + const messages = getModelInputSnippet(model) as ChatCompletionInputMessage[]; + const runCommandInstruct = `# Call the server using curl: +curl -X POST "http://localhost:8000/v1/chat/completions" \\ + -H "Content-Type: application/json" \\ + --data '{ + "model": "${model.id}", + "messages": ${stringifyMessages(messages, { + indent: "\t\t", + attributeKeyQuotes: true, + customContentEscaper: (str) => str.replace(/'/g, "'\\''"), + })} + }'`; + const runCommandNonInstruct = `# Call the server using curl: +curl -X POST "http://localhost:8000/v1/completions" \\ + -H "Content-Type: application/json" \\ + --data '{ + "model": "${model.id}", + "prompt": "Once upon a time,", + "max_tokens": 512, + "temperature": 0.5 + }'`; + const runCommand = model.tags.includes("conversational") ? runCommandInstruct : runCommandNonInstruct; return [ { title: "Install from pip", setup: ["# Install vLLM from pip:", "pip install vllm"].join("\n"), - content: [`# Load and run the model:\nvllm serve "${model.id}"`, runCommand.join("\n")], + content: [`# Load and run the model:\nvllm serve "${model.id}"`, runCommand], }, { title: "Use Docker images", @@ -210,7 +230,7 @@ const snippetVllm = (model: ModelData): LocalAppSnippet[] => { ].join("\n"), content: [ `# Load and run the model:\ndocker exec -it my_vllm_container bash -c "vllm serve ${model.id}"`, - runCommand.join("\n"), + runCommand, ], }, ]; diff --git a/packages/tasks/src/model-data.ts b/packages/tasks/src/model-data.ts index 12c8137d9..975517f3a 100644 --- a/packages/tasks/src/model-data.ts +++ b/packages/tasks/src/model-data.ts @@ -1,6 +1,6 @@ -import type { PipelineType } from "./pipelines"; -import type { WidgetExample } from "./widget-example"; -import type { TokenizerConfig } from "./tokenizer-data"; +import type { PipelineType } from "./pipelines.js"; +import type { WidgetExample } from "./widget-example.js"; +import type { TokenizerConfig } from "./tokenizer-data.js"; /** * Public interface for model metadata diff --git a/packages/tasks/src/model-libraries-snippets.spec.ts b/packages/tasks/src/model-libraries-snippets.spec.ts new file mode 100644 index 000000000..fa87d8242 --- /dev/null +++ b/packages/tasks/src/model-libraries-snippets.spec.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import type { ModelData } from "./model-data.js"; +import { llama_cpp_python } from "./model-libraries-snippets.js"; + +describe("model-libraries-snippets", () => { + it("llama_cpp_python conversational", async () => { + const model: ModelData = { + id: "bartowski/Llama-3.2-3B-Instruct-GGUF", + pipeline_tag: "text-generation", + tags: ["conversational"], + inference: "", + }; + const snippet = llama_cpp_python(model); + + expect(snippet.join("\n")).toEqual(`from llama_cpp import Llama + +llm = Llama.from_pretrained( + repo_id="bartowski/Llama-3.2-3B-Instruct-GGUF", + filename="{{GGUF_FILE}}", +) + +llm.create_chat_completion( + messages = [ + { + "role": "user", + "content": "What is the capital of France?" + } + ] +)`); + }); + + it("llama_cpp_python non-conversational", async () => { + const model: ModelData = { + id: "mlabonne/gemma-2b-GGUF", + tags: [""], + inference: "", + }; + const snippet = llama_cpp_python(model); + + expect(snippet.join("\n")).toEqual(`from llama_cpp import Llama + +llm = Llama.from_pretrained( + repo_id="mlabonne/gemma-2b-GGUF", + filename="{{GGUF_FILE}}", +) + +output = llm( + "Once upon a time,", + max_tokens=512, + echo=True +) +print(output)`); + }); +}); diff --git a/packages/tasks/src/model-libraries-snippets.ts b/packages/tasks/src/model-libraries-snippets.ts index 0be11f247..b0c2ae421 100644 --- a/packages/tasks/src/model-libraries-snippets.ts +++ b/packages/tasks/src/model-libraries-snippets.ts @@ -1,6 +1,9 @@ -import type { ModelData } from "./model-data"; -import type { WidgetExampleTextInput, WidgetExampleSentenceSimilarityInput } from "./widget-example"; -import { LIBRARY_TASK_MAPPING } from "./library-to-tasks"; +import type { ModelData } from "./model-data.js"; +import type { WidgetExampleTextInput, WidgetExampleSentenceSimilarityInput } from "./widget-example.js"; +import { LIBRARY_TASK_MAPPING } from "./library-to-tasks.js"; +import { getModelInputSnippet } from "./snippets/inputs.js"; +import type { ChatCompletionInputMessage } from "./tasks/index.js"; +import { stringifyMessages } from "./snippets/common.js"; const TAG_CUSTOM_CODE = "custom_code"; @@ -363,10 +366,27 @@ export const gliner = (model: ModelData): string[] => [ model = GLiNER.from_pretrained("${model.id}")`, ]; +export const htrflow = (model: ModelData): string[] => [ + `# CLI usage +# see docs: https://ai-riksarkivet.github.io/htrflow/latest/getting_started/quick_start.html +htrflow pipeline `, + `# Python usage +from htrflow.pipeline.pipeline import Pipeline +from htrflow.pipeline.steps import Task +from htrflow.models.framework.model import ModelClass + +pipeline = Pipeline( + [ + Task( + ModelClass, {"model": "${model.id}"}, {} + ), + ])`, +]; + export const keras = (model: ModelData): string[] => [ - `# Available backend options are: "jax", "tensorflow", "torch". + `# Available backend options are: "jax", "torch", "tensorflow". import os -os.environ["KERAS_BACKEND"] = "tensorflow" +os.environ["KERAS_BACKEND"] = "jax" import keras @@ -375,9 +395,9 @@ model = keras.saving.load_model("hf://${model.id}") ]; export const keras_nlp = (model: ModelData): string[] => [ - `# Available backend options are: "jax", "tensorflow", "torch". + `# Available backend options are: "jax", "torch", "tensorflow". import os -os.environ["KERAS_BACKEND"] = "tensorflow" +os.environ["KERAS_BACKEND"] = "jax" import keras_nlp @@ -386,23 +406,48 @@ backbone = keras_nlp.models.Backbone.from_preset("hf://${model.id}") `, ]; -export const llama_cpp_python = (model: ModelData): string[] => [ - `from llama_cpp import Llama +export const keras_hub = (model: ModelData): string[] => [ + `# Available backend options are: "jax", "torch", "tensorflow". +import os +os.environ["KERAS_BACKEND"] = "jax" + +import keras_hub + +# Load a task-specific model (*replace CausalLM with your task*) +model = keras_hub.models.CausalLM.from_preset("hf://${model.id}", dtype="bfloat16") + +# Possible tasks are CausalLM, TextToImage, ImageClassifier, ... +# full list here: https://keras.io/api/keras_hub/models/#api-documentation +`, +]; + +export const llama_cpp_python = (model: ModelData): string[] => { + const snippets = [ + `from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="${model.id}", filename="{{GGUF_FILE}}", ) +`, + ]; -llm.create_chat_completion( - messages = [ - { - "role": "user", - "content": "What is the capital of France?" - } - ] -)`, -]; + if (model.tags.includes("conversational")) { + const messages = getModelInputSnippet(model) as ChatCompletionInputMessage[]; + snippets.push(`llm.create_chat_completion( + messages = ${stringifyMessages(messages, { attributeKeyQuotes: true, indent: "\t" })} +)`); + } else { + snippets.push(`output = llm( + "Once upon a time,", + max_tokens=512, + echo=True +) +print(output)`); + } + + return snippets; +}; export const tf_keras = (model: ModelData): string[] => [ `# Note: 'keras<3.x' or 'tf_keras' must be installed (legacy) @@ -888,10 +933,9 @@ export const peft = (model: ModelData): string[] => { } return [ - `from peft import PeftModel, PeftConfig + `from peft import PeftModel from transformers import AutoModelFor${pefttask} -config = PeftConfig.from_pretrained("${model.id}") base_model = AutoModelFor${pefttask}.from_pretrained("${peftBaseModel}") model = PeftModel.from_pretrained(base_model, "${model.id}")`, ]; @@ -938,6 +982,26 @@ IWorker engine = WorkerFactory.CreateWorker(BackendType.GPUCompute, model); `, ]; +export const sana = (model: ModelData): string[] => [ + ` +# Load the model and infer image from text +import torch +from app.sana_pipeline import SanaPipeline +from torchvision.utils import save_image + +sana = SanaPipeline("configs/sana_config/1024ms/Sana_1600M_img1024.yaml") +sana.from_pretrained("hf://${model.id}") + +image = sana( + prompt='a cyberpunk cat with a neon sign that says "Sana"', + height=1024, + width=1024, + guidance_scale=5.0, + pag_guidance_scale=2.0, + num_inference_steps=18, +) `, +]; + export const vfimamba = (model: ModelData): string[] => [ `from Trainer_finetune import Model @@ -1054,6 +1118,14 @@ model.set_generation_params(duration=5) # generate 5 seconds. descriptions = ['dog barking', 'sirene of an emergency vehicle', 'footsteps in a corridor'] wav = model.generate(descriptions) # generates 3 samples.`, ]; +export const anemoi = (model: ModelData): string[] => [ + `from anemoi.inference.runners.default import DefaultRunner +from anemoi.inference.config import Configuration +# Create Configuration +config = Configuration(checkpoint = {"huggingface":{"repo_id":"${model.id}"}}) +# Load Runner +runner = DefaultRunner(config)`, +]; export const audiocraft = (model: ModelData): string[] => { if (model.tags.includes("musicgen")) { diff --git a/packages/tasks/src/model-libraries.ts b/packages/tasks/src/model-libraries.ts index 600b6e340..7467efb96 100644 --- a/packages/tasks/src/model-libraries.ts +++ b/packages/tasks/src/model-libraries.ts @@ -1,6 +1,6 @@ -import * as snippets from "./model-libraries-snippets"; -import type { ModelData } from "./model-data"; -import type { ElasticSearchQuery } from "./model-libraries-downloads"; +import * as snippets from "./model-libraries-snippets.js"; +import type { ModelData } from "./model-data.js"; +import type { ElasticSearchQuery } from "./model-libraries-downloads.js"; /** * Elements configurable by a model library. @@ -75,6 +75,15 @@ export const MODEL_LIBRARIES_UI_ELEMENTS = { snippets: snippets.allennlp, filter: true, }, + anemoi: { + prettyLabel: "AnemoI", + repoName: "AnemoI", + repoUrl: "https://github.com/ecmwf/anemoi-inference", + docsUrl: "https://anemoi-docs.readthedocs.io/en/latest/", + filter: false, + countDownloads: `path_extension:"ckpt"`, + snippets: snippets.anemoi, + }, asteroid: { prettyLabel: "Asteroid", repoName: "Asteroid", @@ -114,6 +123,13 @@ export const MODEL_LIBRARIES_UI_ELEMENTS = { filter: false, countDownloads: `path_extension:"npz"`, }, + birder: { + prettyLabel: "Birder", + repoName: "Birder", + repoUrl: "https://gitlab.com/birder/birder", + filter: false, + countDownloads: `path_extension:"pt"`, + }, birefnet: { prettyLabel: "BiRefNet", repoName: "BiRefNet", @@ -150,6 +166,13 @@ export const MODEL_LIBRARIES_UI_ELEMENTS = { filter: false, countDownloads: `path:"adapter_config.json"`, }, + "cxr-foundation": { + prettyLabel: "CXR Foundation", + repoName: "cxr-foundation", + repoUrl: "https://github.com/google-health/cxr-foundation", + filter: false, + countDownloads: `path:"precomputed_embeddings/embeddings.npz" OR path:"pax-elixr-b-text/saved_model.pb"`, + }, deepforest: { prettyLabel: "DeepForest", repoName: "deepforest", @@ -173,6 +196,13 @@ export const MODEL_LIBRARIES_UI_ELEMENTS = { snippets: snippets.depth_pro, filter: false, }, + "derm-foundation": { + prettyLabel: "Derm Foundation", + repoName: "derm-foundation", + repoUrl: "https://github.com/google-health/derm-foundation", + filter: false, + countDownloads: `path:"scin_dataset_precomputed_embeddings.npz" OR path:"saved_model.pb"`, + }, diffree: { prettyLabel: "Diffree", repoName: "Diffree", @@ -212,6 +242,13 @@ export const MODEL_LIBRARIES_UI_ELEMENTS = { repoUrl: "https://github.com/cartesia-ai/cartesia_mlx", snippets: snippets.cartesia_mlx, }, + clipscope: { + prettyLabel: "clipscope", + repoName: "clipscope", + repoUrl: "https://github.com/Lewington-pitsos/clipscope", + filter: false, + countDownloads: `path_extension:"pt"`, + }, cosyvoice: { prettyLabel: "CosyVoice", repoName: "CosyVoice", @@ -324,6 +361,13 @@ export const MODEL_LIBRARIES_UI_ELEMENTS = { docsUrl: "https://hezarai.github.io/hezar", countDownloads: `path:"model_config.yaml" OR path:"embedding/embedding_config.yaml"`, }, + htrflow: { + prettyLabel: "HTRflow", + repoName: "HTRflow", + repoUrl: "https://github.com/AI-Riksarkivet/htrflow", + docsUrl: "https://ai-riksarkivet.github.io/htrflow", + snippets: snippets.htrflow, + }, "hunyuan-dit": { prettyLabel: "HunyuanDiT", repoName: "HunyuanDiT", @@ -352,16 +396,23 @@ export const MODEL_LIBRARIES_UI_ELEMENTS = { repoUrl: "https://github.com/keras-team/tf-keras", docsUrl: "https://huggingface.co/docs/hub/tf-keras", snippets: snippets.tf_keras, - filter: true, countDownloads: `path:"saved_model.pb"`, }, "keras-nlp": { prettyLabel: "KerasNLP", repoName: "KerasNLP", - repoUrl: "https://keras.io/keras_nlp/", - docsUrl: "https://github.com/keras-team/keras-nlp", + repoUrl: "https://github.com/keras-team/keras-nlp", + docsUrl: "https://keras.io/keras_nlp/", snippets: snippets.keras_nlp, }, + "keras-hub": { + prettyLabel: "KerasHub", + repoName: "KerasHub", + repoUrl: "https://github.com/keras-team/keras-hub", + docsUrl: "https://keras.io/keras_hub/", + snippets: snippets.keras_hub, + filter: true, + }, k2: { prettyLabel: "K2", repoName: "k2", @@ -380,6 +431,12 @@ export const MODEL_LIBRARIES_UI_ELEMENTS = { repoUrl: "https://github.com/abetlen/llama-cpp-python", snippets: snippets.llama_cpp_python, }, + "mini-omni2": { + prettyLabel: "Mini-Omni2", + repoName: "Mini-Omni2", + repoUrl: "https://github.com/gpt-omni/mini-omni2", + countDownloads: `path:"model_config.yaml"`, + }, mindspore: { prettyLabel: "MindSpore", repoName: "mindspore", @@ -463,6 +520,12 @@ export const MODEL_LIBRARIES_UI_ELEMENTS = { filter: true, countDownloads: `path_extension:"nemo" OR path:"model_config.yaml"`, }, + "open-oasis": { + prettyLabel: "open-oasis", + repoName: "open-oasis", + repoUrl: "https://github.com/etched-ai/open-oasis", + countDownloads: `path:"oasis500m.safetensors"`, + }, open_clip: { prettyLabel: "OpenCLIP", repoName: "OpenCLIP", @@ -687,6 +750,13 @@ export const MODEL_LIBRARIES_UI_ELEMENTS = { filter: false, countDownloads: `path_extension:"safetensors" OR path_extension:"pt"`, }, + genmo: { + prettyLabel: "Genmo", + repoName: "Genmo", + repoUrl: "https://github.com/genmoai/models", + filter: false, + countDownloads: `path:"vae_stats.json"`, + }, tensorflowtts: { prettyLabel: "TensorFlowTTS", repoName: "TensorFlowTTS", @@ -732,11 +802,11 @@ export const MODEL_LIBRARIES_UI_ELEMENTS = { snippets: snippets.transformersJS, filter: true, }, - "trellis": { + trellis: { prettyLabel: "Trellis", repoName: "Trellis", repoUrl: "https://github.com/microsoft/TRELLIS", - countDownloads: `path:"*/model.safetensors"`, + countDownloads: `path_extension:"safetensors"`, }, "unity-sentis": { prettyLabel: "unity-sentis", @@ -746,6 +816,13 @@ export const MODEL_LIBRARIES_UI_ELEMENTS = { filter: true, countDownloads: `path_extension:"sentis"`, }, + sana: { + prettyLabel: "Sana", + repoName: "Sana", + repoUrl: "https://github.com/NVlabs/Sana", + countDownloads: `path_extension:"pth"`, + snippets: snippets.sana, + }, "vfi-mamba": { prettyLabel: "VFIMamba", repoName: "VFIMamba", diff --git a/packages/tasks/src/pipelines.ts b/packages/tasks/src/pipelines.ts index 34907f064..e272d2eef 100644 --- a/packages/tasks/src/pipelines.ts +++ b/packages/tasks/src/pipelines.ts @@ -1,4 +1,4 @@ -export const MODALITIES = ["cv", "nlp", "audio", "tabular", "multimodal", "rl", "other"] as const; +export const MODALITIES = ["multimodal", "nlp", "cv", "audio", "tabular", "rl", "other"] as const; export type Modality = (typeof MODALITIES)[number]; @@ -355,6 +355,12 @@ export const PIPELINE_DATA = { modality: "audio", color: "green", }, + "audio-text-to-text": { + name: "Audio-Text-to-Text", + modality: "multimodal", + color: "red", + hideInDatasets: true, + }, "voice-activity-detection": { name: "Voice Activity Detection", modality: "audio", diff --git a/packages/tasks/src/snippets/common.ts b/packages/tasks/src/snippets/common.ts index b0fe4bef5..fe5452f69 100644 --- a/packages/tasks/src/snippets/common.ts +++ b/packages/tasks/src/snippets/common.ts @@ -1,63 +1,39 @@ -import type { ChatCompletionInputMessage, GenerationParameters } from "../tasks"; - -export interface StringifyMessagesOptions { - sep: string; - start: string; - end: string; - attributeKeyQuotes?: boolean; - customContentEscaper?: (str: string) => string; -} - -export function stringifyMessages(messages: ChatCompletionInputMessage[], opts: StringifyMessagesOptions): string { - const keyRole = opts.attributeKeyQuotes ? `"role"` : "role"; - const keyContent = opts.attributeKeyQuotes ? `"content"` : "content"; - - const messagesStringified = messages.map(({ role, content }) => { - if (typeof content === "string") { - content = JSON.stringify(content).slice(1, -1); - if (opts.customContentEscaper) { - content = opts.customContentEscaper(content); - } - return `{ ${keyRole}: "${role}", ${keyContent}: "${content}" }`; - } else { - 2; - content = content.map(({ image_url, text, type }) => ({ - type, - image_url, - ...(text ? { text: JSON.stringify(text).slice(1, -1) } : undefined), - })); - content = JSON.stringify(content).slice(1, -1); - if (opts.customContentEscaper) { - content = opts.customContentEscaper(content); - } - return `{ ${keyRole}: "${role}", ${keyContent}: ${content} }`; - } - }); - - return opts.start + messagesStringified.join(opts.sep) + opts.end; +import type { ChatCompletionInputMessage, GenerationParameters } from "../tasks/index.js"; + +export function stringifyMessages( + messages: ChatCompletionInputMessage[], + opts?: { + indent?: string; + attributeKeyQuotes?: boolean; + customContentEscaper?: (str: string) => string; + } +): string { + let messagesStr = JSON.stringify(messages, null, "\t"); + if (opts?.indent) { + messagesStr = messagesStr.replaceAll("\n", `\n${opts.indent}`); + } + if (!opts?.attributeKeyQuotes) { + messagesStr = messagesStr.replace(/"([^"]+)":/g, "$1:"); + } + if (opts?.customContentEscaper) { + messagesStr = opts.customContentEscaper(messagesStr); + } + return messagesStr; } type PartialGenerationParameters = Partial>; -export interface StringifyGenerationConfigOptions { - sep: string; - start: string; - end: string; - attributeValueConnector: string; - attributeKeyQuotes?: boolean; -} - export function stringifyGenerationConfig( config: PartialGenerationParameters, - opts: StringifyGenerationConfigOptions + opts: { + indent: string; + attributeValueConnector: string; + attributeKeyQuotes?: boolean; + } ): string { const quote = opts.attributeKeyQuotes ? `"` : ""; - return ( - opts.start + - Object.entries(config) - .map(([key, val]) => `${quote}${key}${quote}${opts.attributeValueConnector}${val}`) - .join(opts.sep) + - opts.end - ); + return Object.entries(config) + .map(([key, val]) => `${quote}${key}${quote}${opts.attributeValueConnector}${val}`) + .join(`,${opts.indent}`); } diff --git a/packages/tasks/src/snippets/curl.ts b/packages/tasks/src/snippets/curl.ts index a6bdfcb15..f3ba735f3 100644 --- a/packages/tasks/src/snippets/curl.ts +++ b/packages/tasks/src/snippets/curl.ts @@ -9,7 +9,7 @@ export const snippetBasic = (model: ModelDataMinimal, accessToken: string): Infe -X POST \\ -d '{"inputs": ${getModelInputSnippet(model, true)}}' \\ -H 'Content-Type: application/json' \\ - -H "Authorization: Bearer ${accessToken || `{API_TOKEN}`}"`, + -H 'Authorization: Bearer ${accessToken || `{API_TOKEN}`}'`, }); export const snippetTextGeneration = ( @@ -26,9 +26,8 @@ export const snippetTextGeneration = ( if (model.tags.includes("conversational")) { // Conversational model detected, so we display a code snippet that features the Messages API const streaming = opts?.streaming ?? true; - const messages: ChatCompletionInputMessage[] = opts?.messages ?? [ - { role: "user", content: "What is the capital of France?" }, - ]; + const exampleMessages = getModelInputSnippet(model) as ChatCompletionInputMessage[]; + const messages = opts?.messages ?? exampleMessages; const config = { ...(opts?.temperature ? { temperature: opts.temperature } : undefined), @@ -37,21 +36,17 @@ export const snippetTextGeneration = ( }; return { content: `curl 'https://api-inference.huggingface.co/models/${model.id}/v1/chat/completions' \\ --H "Authorization: Bearer ${accessToken || `{API_TOKEN}`}" \\ +-H 'Authorization: Bearer ${accessToken || `{API_TOKEN}`}' \\ -H 'Content-Type: application/json' \\ --data '{ "model": "${model.id}", "messages": ${stringifyMessages(messages, { - sep: ",\n\t\t", - start: `[\n\t\t`, - end: `\n\t]`, + indent: "\t", attributeKeyQuotes: true, customContentEscaper: (str) => str.replace(/'/g, "'\\''"), })}, ${stringifyGenerationConfig(config, { - sep: ",\n ", - start: "", - end: "", + indent: "\n ", attributeKeyQuotes: true, attributeValueConnector: ": ", })}, @@ -63,47 +58,19 @@ export const snippetTextGeneration = ( } }; -export const snippetImageTextToTextGeneration = (model: ModelDataMinimal, accessToken: string): InferenceSnippet => { - if (model.tags.includes("conversational")) { - // Conversational model detected, so we display a code snippet that features the Messages API - return { - content: `curl 'https://api-inference.huggingface.co/models/${model.id}/v1/chat/completions' \\ --H "Authorization: Bearer ${accessToken || `{API_TOKEN}`}" \\ --H 'Content-Type: application/json' \\ --d '{ - "model": "${model.id}", - "messages": [ - { - "role": "user", - "content": [ - {"type": "image_url", "image_url": {"url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg"}}, - {"type": "text", "text": "Describe this image in one sentence."} - ] - } - ], - "max_tokens": 500, - "stream": false -}' -`, - }; - } else { - return snippetBasic(model, accessToken); - } -}; - export const snippetZeroShotClassification = (model: ModelDataMinimal, accessToken: string): InferenceSnippet => ({ content: `curl https://api-inference.huggingface.co/models/${model.id} \\ -X POST \\ -d '{"inputs": ${getModelInputSnippet(model, true)}, "parameters": {"candidate_labels": ["refund", "legal", "faq"]}}' \\ -H 'Content-Type: application/json' \\ - -H "Authorization: Bearer ${accessToken || `{API_TOKEN}`}"`, + -H 'Authorization: Bearer ${accessToken || `{API_TOKEN}`}'`, }); export const snippetFile = (model: ModelDataMinimal, accessToken: string): InferenceSnippet => ({ content: `curl https://api-inference.huggingface.co/models/${model.id} \\ -X POST \\ --data-binary '@${getModelInputSnippet(model, true, true)}' \\ - -H "Authorization: Bearer ${accessToken || `{API_TOKEN}`}"`, + -H 'Authorization: Bearer ${accessToken || `{API_TOKEN}`}'`, }); export const curlSnippets: Partial< @@ -122,7 +89,7 @@ export const curlSnippets: Partial< summarization: snippetBasic, "feature-extraction": snippetBasic, "text-generation": snippetTextGeneration, - "image-text-to-text": snippetImageTextToTextGeneration, + "image-text-to-text": snippetTextGeneration, "text2text-generation": snippetBasic, "fill-mask": snippetBasic, "sentence-similarity": snippetBasic, @@ -138,9 +105,13 @@ export const curlSnippets: Partial< "image-segmentation": snippetFile, }; -export function getCurlInferenceSnippet(model: ModelDataMinimal, accessToken: string): InferenceSnippet { +export function getCurlInferenceSnippet( + model: ModelDataMinimal, + accessToken: string, + opts?: Record +): InferenceSnippet { return model.pipeline_tag && model.pipeline_tag in curlSnippets - ? curlSnippets[model.pipeline_tag]?.(model, accessToken) ?? { content: "" } + ? curlSnippets[model.pipeline_tag]?.(model, accessToken, opts) ?? { content: "" } : { content: "" }; } diff --git a/packages/tasks/src/snippets/index.ts b/packages/tasks/src/snippets/index.ts index 3cf9b9d23..c37645815 100644 --- a/packages/tasks/src/snippets/index.ts +++ b/packages/tasks/src/snippets/index.ts @@ -1,6 +1,7 @@ -import * as inputs from "./inputs"; -import * as curl from "./curl"; -import * as python from "./python"; -import * as js from "./js"; +import * as inputs from "./inputs.js"; +import * as curl from "./curl.js"; +import * as python from "./python.js"; +import * as js from "./js.js"; +export * from "./types.js"; export { inputs, curl, python, js }; diff --git a/packages/tasks/src/snippets/inputs.ts b/packages/tasks/src/snippets/inputs.ts index f3c76d12c..49e270dff 100644 --- a/packages/tasks/src/snippets/inputs.ts +++ b/packages/tasks/src/snippets/inputs.ts @@ -1,5 +1,6 @@ -import type { PipelineType } from "../pipelines"; -import type { ModelDataMinimal } from "./types"; +import type { PipelineType } from "../pipelines.js"; +import type { ChatCompletionInputMessage } from "../tasks/index.js"; +import type { ModelDataMinimal } from "./types.js"; const inputsZeroShotClassification = () => `"Hi, I recently bought a device from your company but it is not working as advertised and I would like to get reimbursed!"`; @@ -40,7 +41,30 @@ const inputsTextClassification = () => `"I like you. I love you"`; const inputsTokenClassification = () => `"My name is Sarah Jessica Parker but you can call me Jessica"`; -const inputsTextGeneration = () => `"Can you please let us know more details about your "`; +const inputsTextGeneration = (model: ModelDataMinimal): string | ChatCompletionInputMessage[] => { + if (model.tags.includes("conversational")) { + return model.pipeline_tag === "text-generation" + ? [{ role: "user", content: "What is the capital of France?" }] + : [ + { + role: "user", + content: [ + { + type: "text", + text: "Describe this image in one sentence.", + }, + { + type: "image_url", + image_url: { + url: "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg", + }, + }, + ], + }, + ]; + } + return `"Can you please let us know more details about your "`; +}; const inputsText2TextGeneration = () => `"The answer to the universe is"`; @@ -84,7 +108,7 @@ const inputsTabularPrediction = () => const inputsZeroShotImageClassification = () => `"cats.jpg"`; const modelInputSnippets: { - [key in PipelineType]?: (model: ModelDataMinimal) => string; + [key in PipelineType]?: (model: ModelDataMinimal) => string | ChatCompletionInputMessage[]; } = { "audio-to-audio": inputsAudioToAudio, "audio-classification": inputsAudioClassification, @@ -104,6 +128,7 @@ const modelInputSnippets: { "tabular-classification": inputsTabularPrediction, "text-classification": inputsTextClassification, "text-generation": inputsTextGeneration, + "image-text-to-text": inputsTextGeneration, "text-to-image": inputsTextToImage, "text-to-speech": inputsTextToSpeech, "text-to-audio": inputsTextToAudio, @@ -116,18 +141,24 @@ const modelInputSnippets: { // Use noWrap to put the whole snippet on a single line (removing new lines and tabulations) // Use noQuotes to strip quotes from start & end (example: "abc" -> abc) -export function getModelInputSnippet(model: ModelDataMinimal, noWrap = false, noQuotes = false): string { +export function getModelInputSnippet( + model: ModelDataMinimal, + noWrap = false, + noQuotes = false +): string | ChatCompletionInputMessage[] { if (model.pipeline_tag) { const inputs = modelInputSnippets[model.pipeline_tag]; if (inputs) { let result = inputs(model); - if (noWrap) { - result = result.replace(/(?:(?:\r?\n|\r)\t*)|\t+/g, " "); - } - if (noQuotes) { - const REGEX_QUOTES = /^"(.+)"$/s; - const match = result.match(REGEX_QUOTES); - result = match ? match[1] : result; + if (typeof result === "string") { + if (noWrap) { + result = result.replace(/(?:(?:\r?\n|\r)\t*)|\t+/g, " "); + } + if (noQuotes) { + const REGEX_QUOTES = /^"(.+)"$/s; + const match = result.match(REGEX_QUOTES); + result = match ? match[1] : result; + } } return result; } diff --git a/packages/tasks/src/snippets/js.ts b/packages/tasks/src/snippets/js.ts index f735ca912..970752582 100644 --- a/packages/tasks/src/snippets/js.ts +++ b/packages/tasks/src/snippets/js.ts @@ -10,7 +10,7 @@ export const snippetBasic = (model: ModelDataMinimal, accessToken: string): Infe "https://api-inference.huggingface.co/models/${model.id}", { headers: { - Authorization: "Bearer ${accessToken || `{API_TOKEN}`}" + Authorization: "Bearer ${accessToken || `{API_TOKEN}`}", "Content-Type": "application/json", }, method: "POST", @@ -40,10 +40,9 @@ export const snippetTextGeneration = ( if (model.tags.includes("conversational")) { // Conversational model detected, so we display a code snippet that features the Messages API const streaming = opts?.streaming ?? true; - const messages: ChatCompletionInputMessage[] = opts?.messages ?? [ - { role: "user", content: "What is the capital of France?" }, - ]; - const messagesStr = stringifyMessages(messages, { sep: ",\n\t\t", start: "[\n\t\t", end: "\n\t]" }); + const exampleMessages = getModelInputSnippet(model) as ChatCompletionInputMessage[]; + const messages = opts?.messages ?? exampleMessages; + const messagesStr = stringifyMessages(messages, { indent: "\t" }); const config = { ...(opts?.temperature ? { temperature: opts.temperature } : undefined), @@ -51,19 +50,17 @@ export const snippetTextGeneration = ( ...(opts?.top_p ? { top_p: opts.top_p } : undefined), }; const configStr = stringifyGenerationConfig(config, { - sep: ",\n\t", - start: "", - end: "", + indent: "\n\t", attributeValueConnector: ": ", }); if (streaming) { return [ { - client: "huggingface_hub", - content: `import { HfInference } from "@huggingface/inference" + client: "huggingface.js", + content: `import { HfInference } from "@huggingface/inference"; -const client = new HfInference("${accessToken || `{API_TOKEN}`}") +const client = new HfInference("${accessToken || `{API_TOKEN}`}"); let out = ""; @@ -83,12 +80,12 @@ for await (const chunk of stream) { }, { client: "openai", - content: `import { OpenAI } from "openai" + content: `import { OpenAI } from "openai"; const client = new OpenAI({ baseURL: "https://api-inference.huggingface.co/v1/", apiKey: "${accessToken || `{API_TOKEN}`}" -}) +}); let out = ""; @@ -111,10 +108,10 @@ for await (const chunk of stream) { } else { return [ { - client: "huggingface_hub", - content: `import { HfInference } from '@huggingface/inference' + client: "huggingface.js", + content: `import { HfInference } from "@huggingface/inference"; -const client = new HfInference("${accessToken || `{API_TOKEN}`}") +const client = new HfInference("${accessToken || `{API_TOKEN}`}"); const chatCompletion = await client.chatCompletion({ model: "${model.id}", @@ -126,12 +123,12 @@ console.log(chatCompletion.choices[0].message);`, }, { client: "openai", - content: `import { OpenAI } from "openai" + content: `import { OpenAI } from "openai"; const client = new OpenAI({ baseURL: "https://api-inference.huggingface.co/v1/", apiKey: "${accessToken || `{API_TOKEN}`}" -}) +}); const chatCompletion = await client.chat.completions.create({ model: "${model.id}", @@ -148,43 +145,13 @@ console.log(chatCompletion.choices[0].message);`, } }; -export const snippetImageTextToTextGeneration = (model: ModelDataMinimal, accessToken: string): InferenceSnippet => { - if (model.tags.includes("conversational")) { - // Conversational model detected, so we display a code snippet that features the Messages API - return { - content: `import { HfInference } from "@huggingface/inference"; - -const inference = new HfInference("${accessToken || `{API_TOKEN}`}"); -const imageUrl = "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg"; - -for await (const chunk of inference.chatCompletionStream({ - model: "${model.id}", - messages: [ - { - "role": "user", - "content": [ - {"type": "image_url", "image_url": {"url": imageUrl}}, - {"type": "text", "text": "Describe this image in one sentence."}, - ], - } - ], - max_tokens: 500, -})) { - process.stdout.write(chunk.choices[0]?.delta?.content || ""); -}`, - }; - } else { - return snippetBasic(model, accessToken); - } -}; - export const snippetZeroShotClassification = (model: ModelDataMinimal, accessToken: string): InferenceSnippet => ({ content: `async function query(data) { const response = await fetch( "https://api-inference.huggingface.co/models/${model.id}", { headers: { - Authorization: "Bearer ${accessToken || `{API_TOKEN}`}" + Authorization: "Bearer ${accessToken || `{API_TOKEN}`}", "Content-Type": "application/json", }, method: "POST", @@ -208,7 +175,7 @@ export const snippetTextToImage = (model: ModelDataMinimal, accessToken: string) "https://api-inference.huggingface.co/models/${model.id}", { headers: { - Authorization: "Bearer ${accessToken || `{API_TOKEN}`}" + Authorization: "Bearer ${accessToken || `{API_TOKEN}`}", "Content-Type": "application/json", }, method: "POST", @@ -229,7 +196,7 @@ export const snippetTextToAudio = (model: ModelDataMinimal, accessToken: string) "https://api-inference.huggingface.co/models/${model.id}", { headers: { - Authorization: "Bearer ${accessToken || `{API_TOKEN}`}" + Authorization: "Bearer ${accessToken || `{API_TOKEN}`}", "Content-Type": "application/json", }, method: "POST", @@ -271,7 +238,7 @@ export const snippetFile = (model: ModelDataMinimal, accessToken: string): Infer "https://api-inference.huggingface.co/models/${model.id}", { headers: { - Authorization: "Bearer ${accessToken || `{API_TOKEN}`}" + Authorization: "Bearer ${accessToken || `{API_TOKEN}`}", "Content-Type": "application/json", }, method: "POST", @@ -307,7 +274,7 @@ export const jsSnippets: Partial< summarization: snippetBasic, "feature-extraction": snippetBasic, "text-generation": snippetTextGeneration, - "image-text-to-text": snippetImageTextToTextGeneration, + "image-text-to-text": snippetTextGeneration, "text2text-generation": snippetBasic, "fill-mask": snippetBasic, "sentence-similarity": snippetBasic, @@ -325,10 +292,11 @@ export const jsSnippets: Partial< export function getJsInferenceSnippet( model: ModelDataMinimal, - accessToken: string + accessToken: string, + opts?: Record ): InferenceSnippet | InferenceSnippet[] { return model.pipeline_tag && model.pipeline_tag in jsSnippets - ? jsSnippets[model.pipeline_tag]?.(model, accessToken) ?? { content: "" } + ? jsSnippets[model.pipeline_tag]?.(model, accessToken, opts) ?? { content: "" } : { content: "" }; } diff --git a/packages/tasks/src/snippets/python.ts b/packages/tasks/src/snippets/python.ts index 2cb69693e..bdb148e39 100644 --- a/packages/tasks/src/snippets/python.ts +++ b/packages/tasks/src/snippets/python.ts @@ -4,6 +4,11 @@ import { stringifyGenerationConfig, stringifyMessages } from "./common.js"; import { getModelInputSnippet } from "./inputs.js"; import type { InferenceSnippet, ModelDataMinimal } from "./types.js"; +const snippetImportInferenceClient = (model: ModelDataMinimal, accessToken: string): string => + `from huggingface_hub import InferenceClient +client = InferenceClient("${model.id}", token="${accessToken || "{API_TOKEN}"}") +`; + export const snippetConversational = ( model: ModelDataMinimal, accessToken: string, @@ -16,15 +21,9 @@ export const snippetConversational = ( } ): InferenceSnippet[] => { const streaming = opts?.streaming ?? true; - const messages: ChatCompletionInputMessage[] = opts?.messages ?? [ - { role: "user", content: "What is the capital of France?" }, - ]; - const messagesStr = stringifyMessages(messages, { - sep: ",\n\t", - start: `[\n\t`, - end: `\n]`, - attributeKeyQuotes: true, - }); + const exampleMessages = getModelInputSnippet(model) as ChatCompletionInputMessage[]; + const messages = opts?.messages ?? exampleMessages; + const messagesStr = stringifyMessages(messages, { attributeKeyQuotes: true }); const config = { ...(opts?.temperature ? { temperature: opts.temperature } : undefined), @@ -32,9 +31,7 @@ export const snippetConversational = ( ...(opts?.top_p ? { top_p: opts.top_p } : undefined), }; const configStr = stringifyGenerationConfig(config, { - sep: ",\n\t", - start: "", - end: "", + indent: "\n\t", attributeValueConnector: "=", }); @@ -56,7 +53,7 @@ stream = client.chat.completions.create( ) for chunk in stream: - print(chunk.choices[0].delta.content)`, + print(chunk.choices[0].delta.content, end="")`, }, { client: "openai", @@ -77,7 +74,7 @@ stream = client.chat.completions.create( ) for chunk in stream: - print(chunk.choices[0].delta.content)`, + print(chunk.choices[0].delta.content, end="")`, }, ]; } else { @@ -121,30 +118,6 @@ print(completion.choices[0].message)`, } }; -export const snippetConversationalWithImage = (model: ModelDataMinimal, accessToken: string): InferenceSnippet => ({ - content: `from huggingface_hub import InferenceClient - -client = InferenceClient(api_key="${accessToken || "{API_TOKEN}"}") - -image_url = "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" - -for message in client.chat_completion( - model="${model.id}", - messages=[ - { - "role": "user", - "content": [ - {"type": "image_url", "image_url": {"url": image_url}}, - {"type": "text", "text": "Describe this image in one sentence."}, - ], - } - ], - max_tokens=500, - stream=True, -): - print(message.choices[0].delta.content, end="")`, -}); - export const snippetZeroShotClassification = (model: ModelDataMinimal): InferenceSnippet => ({ content: `def query(payload): response = requests.post(API_URL, headers=headers, json=payload) @@ -193,18 +166,28 @@ export const snippetFile = (model: ModelDataMinimal): InferenceSnippet => ({ output = query(${getModelInputSnippet(model)})`, }); -export const snippetTextToImage = (model: ModelDataMinimal): InferenceSnippet => ({ - content: `def query(payload): +export const snippetTextToImage = (model: ModelDataMinimal, accessToken: string): InferenceSnippet[] => [ + { + client: "huggingface_hub", + content: `${snippetImportInferenceClient(model, accessToken)} +# output is a PIL.Image object +image = client.text_to_image(${getModelInputSnippet(model)})`, + }, + { + client: "requests", + content: `def query(payload): response = requests.post(API_URL, headers=headers, json=payload) return response.content image_bytes = query({ "inputs": ${getModelInputSnippet(model)}, }) + # You can access the image with PIL.Image for example import io from PIL import Image image = Image.open(io.BytesIO(image_bytes))`, -}); + }, +]; export const snippetTabular = (model: ModelDataMinimal): InferenceSnippet => ({ content: `def query(payload): @@ -282,7 +265,7 @@ export const pythonSnippets: Partial< "feature-extraction": snippetBasic, "text-generation": snippetBasic, "text2text-generation": snippetBasic, - "image-text-to-text": snippetConversationalWithImage, + "image-text-to-text": snippetConversational, "fill-mask": snippetBasic, "sentence-similarity": snippetBasic, "automatic-speech-recognition": snippetFile, @@ -306,12 +289,9 @@ export function getPythonInferenceSnippet( accessToken: string, opts?: Record ): InferenceSnippet | InferenceSnippet[] { - if (model.pipeline_tag === "text-generation" && model.tags.includes("conversational")) { + if (model.tags.includes("conversational")) { // Conversational model detected, so we display a code snippet that features the Messages API return snippetConversational(model, accessToken, opts); - } else if (model.pipeline_tag === "image-text-to-text" && model.tags.includes("conversational")) { - // Example sending an image to the Message API - return snippetConversationalWithImage(model, accessToken); } else { let snippets = model.pipeline_tag && model.pipeline_tag in pythonSnippets @@ -323,12 +303,14 @@ export function getPythonInferenceSnippet( return snippets.map((snippet) => { return { ...snippet, - content: `import requests + content: snippet.content.includes("requests") + ? `import requests API_URL = "https://api-inference.huggingface.co/models/${model.id}" headers = {"Authorization": ${accessToken ? `"Bearer ${accessToken}"` : `f"Bearer {API_TOKEN}"`}} -${snippet.content}`, +${snippet.content}` + : snippet.content, }; }); } diff --git a/packages/tasks/src/snippets/types.ts b/packages/tasks/src/snippets/types.ts index 230fdc9b8..c6a78c278 100644 --- a/packages/tasks/src/snippets/types.ts +++ b/packages/tasks/src/snippets/types.ts @@ -1,4 +1,4 @@ -import type { ModelData } from "../model-data"; +import type { ModelData } from "../model-data.js"; /** * Minimal model data required for snippets. diff --git a/packages/tasks/src/tasks/audio-classification/data.ts b/packages/tasks/src/tasks/audio-classification/data.ts index be123d341..8919f81c0 100644 --- a/packages/tasks/src/tasks/audio-classification/data.ts +++ b/packages/tasks/src/tasks/audio-classification/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/audio-classification/inference.ts b/packages/tasks/src/tasks/audio-classification/inference.ts index b8ed3f50f..9d3bfb1da 100644 --- a/packages/tasks/src/tasks/audio-classification/inference.ts +++ b/packages/tasks/src/tasks/audio-classification/inference.ts @@ -13,17 +13,18 @@ export interface AudioClassificationInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Audio Classification */ parameters?: AudioClassificationParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Audio Classification */ export interface AudioClassificationParameters { + /** + * The function to apply to the model outputs in order to retrieve the scores. + */ function_to_apply?: ClassificationOutputTransform; /** * When specified, limits the output to the top K most probable classes. diff --git a/packages/tasks/src/tasks/audio-classification/spec/input.json b/packages/tasks/src/tasks/audio-classification/spec/input.json index 0bfeb69cd..dc8036b9b 100644 --- a/packages/tasks/src/tasks/audio-classification/spec/input.json +++ b/packages/tasks/src/tasks/audio-classification/spec/input.json @@ -10,19 +10,19 @@ "type": "string" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Audio Classification", "$ref": "#/$defs/AudioClassificationParameters" } }, "$defs": { "AudioClassificationParameters": { "title": "AudioClassificationParameters", - "description": "Additional inference parameters for Audio Classification", "type": "object", "properties": { "function_to_apply": { "title": "AudioClassificationOutputTransform", - "$ref": "/inference/schemas/common-definitions.json#/definitions/ClassificationOutputTransform" + "$ref": "/inference/schemas/common-definitions.json#/definitions/ClassificationOutputTransform", + "description": "The function to apply to the model outputs in order to retrieve the scores." }, "top_k": { "type": "integer", diff --git a/packages/tasks/src/tasks/audio-to-audio/data.ts b/packages/tasks/src/tasks/audio-to-audio/data.ts index 9d92983b2..fa1c3a508 100644 --- a/packages/tasks/src/tasks/audio-to-audio/data.ts +++ b/packages/tasks/src/tasks/audio-to-audio/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/automatic-speech-recognition/data.ts b/packages/tasks/src/tasks/automatic-speech-recognition/data.ts index 89078ce71..b177a7f12 100644 --- a/packages/tasks/src/tasks/automatic-speech-recognition/data.ts +++ b/packages/tasks/src/tasks/automatic-speech-recognition/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ @@ -6,12 +6,16 @@ const taskData: TaskDataCustom = { description: "31,175 hours of multilingual audio-text dataset in 108 languages.", id: "mozilla-foundation/common_voice_17_0", }, + { + description: "Multilingual and diverse audio dataset with 101k hours of audio.", + id: "amphion/Emilia-Dataset", + }, { description: "A dataset with 44.6k hours of English speaker data and 6k hours of other language speakers.", id: "parler-tts/mls_eng", }, { - description: "A multi-lingual audio dataset with 370K hours of audio.", + description: "A multilingual audio dataset with 370K hours of audio.", id: "espnet/yodas", }, ], @@ -54,6 +58,10 @@ const taskData: TaskDataCustom = { description: "An end-to-end model that performs ASR and Speech Translation by MetaAI.", id: "facebook/seamless-m4t-v2-large", }, + { + description: "A powerful multilingual ASR and Speech Translation model by Nvidia.", + id: "nvidia/canary-1b", + }, { description: "Powerful speaker diarization model.", id: "pyannote/speaker-diarization-3.1", @@ -65,13 +73,17 @@ const taskData: TaskDataCustom = { id: "hf-audio/whisper-large-v3", }, { - description: "Fastest speech recognition application.", - id: "sanchit-gandhi/whisper-jax", + description: "Latest ASR model from Useful Sensors.", + id: "mrfakename/Moonshinex", }, { description: "A high quality speech and text translation model by Meta.", id: "facebook/seamless_m4t", }, + { + description: "A powerful multilingual ASR and Speech Translation model by Nvidia", + id: "nvidia/canary-1b", + }, ], summary: "Automatic Speech Recognition (ASR), also known as Speech to Text (STT), is the task of transcribing a given audio to text. It has many applications, such as voice user interfaces.", diff --git a/packages/tasks/src/tasks/automatic-speech-recognition/inference.ts b/packages/tasks/src/tasks/automatic-speech-recognition/inference.ts index e1f1a8433..d105c16e8 100644 --- a/packages/tasks/src/tasks/automatic-speech-recognition/inference.ts +++ b/packages/tasks/src/tasks/automatic-speech-recognition/inference.ts @@ -14,15 +14,13 @@ export interface AutomaticSpeechRecognitionInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Automatic Speech Recognition */ parameters?: AutomaticSpeechRecognitionParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Automatic Speech Recognition */ export interface AutomaticSpeechRecognitionParameters { @@ -39,8 +37,6 @@ export interface AutomaticSpeechRecognitionParameters { /** * Parametrization of the text generation process - * - * Ad-hoc parametrization of the text generation process */ export interface GenerationParameters { /** diff --git a/packages/tasks/src/tasks/automatic-speech-recognition/spec/input.json b/packages/tasks/src/tasks/automatic-speech-recognition/spec/input.json index a028bb352..98f1bdf5b 100644 --- a/packages/tasks/src/tasks/automatic-speech-recognition/spec/input.json +++ b/packages/tasks/src/tasks/automatic-speech-recognition/spec/input.json @@ -10,14 +10,13 @@ "type": "string" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Automatic Speech Recognition", "$ref": "#/$defs/AutomaticSpeechRecognitionParameters" } }, "$defs": { "AutomaticSpeechRecognitionParameters": { "title": "AutomaticSpeechRecognitionParameters", - "description": "Additional inference parameters for Automatic Speech Recognition", "type": "object", "properties": { "return_timestamps": { diff --git a/packages/tasks/src/tasks/chat-completion/inference.ts b/packages/tasks/src/tasks/chat-completion/inference.ts index febaffc8f..4c4449a73 100644 --- a/packages/tasks/src/tasks/chat-completion/inference.ts +++ b/packages/tasks/src/tasks/chat-completion/inference.ts @@ -79,7 +79,7 @@ export interface ChatCompletionInput { * We generally recommend altering this or `top_p` but not both. */ temperature?: number; - tool_choice?: ChatCompletionInputTool; + tool_choice?: ChatCompletionInputToolChoice; /** * A prompt to be appended before the tools */ @@ -89,7 +89,7 @@ export interface ChatCompletionInput { * Use this to provide a list of * functions the model may generate JSON inputs for. */ - tools?: ToolElement[]; + tools?: ChatCompletionInputTool[]; /** * An integer between 0 and 5 specifying the number of most likely tokens to return at each * token position, each with @@ -154,10 +154,23 @@ export interface ChatCompletionInputStreamOptions { [property: string]: unknown; } -export type ChatCompletionInputTool = ChatCompletionInputToolType | string; +/** + * + * + */ +export type ChatCompletionInputToolChoice = ChatCompletionInputToolChoiceEnum | ChatCompletionInputToolChoiceObject; + +/** + * Means the model can pick between generating a message or calling one or more tools. + * + * Means the model will not call any tool and instead generates a message. + * + * Means the model must call one or more tools. + */ +export type ChatCompletionInputToolChoiceEnum = "auto" | "none" | "required"; -export interface ChatCompletionInputToolType { - function?: ChatCompletionInputFunctionName; +export interface ChatCompletionInputToolChoiceObject { + function: ChatCompletionInputFunctionName; [property: string]: unknown; } @@ -166,7 +179,7 @@ export interface ChatCompletionInputFunctionName { [property: string]: unknown; } -export interface ToolElement { +export interface ChatCompletionInputTool { function: ChatCompletionInputFunctionDefinition; type: string; [property: string]: unknown; diff --git a/packages/tasks/src/tasks/chat-completion/spec/input.json b/packages/tasks/src/tasks/chat-completion/spec/input.json index 86ca23c82..a4a9ab5fe 100644 --- a/packages/tasks/src/tasks/chat-completion/spec/input.json +++ b/packages/tasks/src/tasks/chat-completion/spec/input.json @@ -32,6 +32,7 @@ "type": "integer", "format": "int32", "description": "The maximum number of tokens that can be generated in the chat completion.", + "default": "1024", "example": "32", "nullable": true, "minimum": 0 @@ -114,6 +115,7 @@ "$ref": "#/$defs/ChatCompletionInputToolChoice" } ], + "default": "auto", "nullable": true }, "tool_prompt": { @@ -272,23 +274,21 @@ "title": "ChatCompletionInputStreamOptions" }, "ChatCompletionInputToolChoice": { - "allOf": [ - { - "$ref": "#/$defs/ChatCompletionInputToolType" - } - ], - "nullable": true, - "title": "ChatCompletionInputToolChoice" - }, - "ChatCompletionInputToolType": { "oneOf": [ { - "type": "object", - "default": null, - "nullable": true + "type": "string", + "description": "Means the model can pick between generating a message or calling one or more tools.", + "enum": ["auto"] }, { - "type": "string" + "type": "string", + "description": "Means the model will not call any tool and instead generates a message.", + "enum": ["none"] + }, + { + "type": "string", + "description": "Means the model must call one or more tools.", + "enum": ["required"] }, { "type": "object", @@ -298,14 +298,10 @@ "$ref": "#/$defs/ChatCompletionInputFunctionName" } } - }, - { - "type": "object", - "default": null, - "nullable": true } ], - "title": "ChatCompletionInputToolType" + "description": "", + "title": "ChatCompletionInputToolChoice" }, "ChatCompletionInputFunctionName": { "type": "object", diff --git a/packages/tasks/src/tasks/common-definitions.json b/packages/tasks/src/tasks/common-definitions.json index 03758a422..a9603fc06 100644 --- a/packages/tasks/src/tasks/common-definitions.json +++ b/packages/tasks/src/tasks/common-definitions.json @@ -26,7 +26,6 @@ }, "GenerationParameters": { "title": "GenerationParameters", - "description": "Ad-hoc parametrization of the text generation process", "type": "object", "properties": { "temperature": { diff --git a/packages/tasks/src/tasks/depth-estimation/data.ts b/packages/tasks/src/tasks/depth-estimation/data.ts index 735f1ccb5..390449149 100644 --- a/packages/tasks/src/tasks/depth-estimation/data.ts +++ b/packages/tasks/src/tasks/depth-estimation/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/depth-estimation/inference.ts b/packages/tasks/src/tasks/depth-estimation/inference.ts index f873f9254..6b2cff1ff 100644 --- a/packages/tasks/src/tasks/depth-estimation/inference.ts +++ b/packages/tasks/src/tasks/depth-estimation/inference.ts @@ -13,7 +13,7 @@ export interface DepthEstimationInput { */ inputs: unknown; /** - * Additional inference parameters + * Additional inference parameters for Depth Estimation */ parameters?: { [key: string]: unknown }; [property: string]: unknown; diff --git a/packages/tasks/src/tasks/depth-estimation/spec/input.json b/packages/tasks/src/tasks/depth-estimation/spec/input.json index 2a4ecc71c..fb6c70bdd 100644 --- a/packages/tasks/src/tasks/depth-estimation/spec/input.json +++ b/packages/tasks/src/tasks/depth-estimation/spec/input.json @@ -9,14 +9,13 @@ "description": "The input image data" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Depth Estimation", "$ref": "#/$defs/DepthEstimationParameters" } }, "$defs": { "DepthEstimationParameters": { "title": "DepthEstimationParameters", - "description": "Additional inference parameters for Depth Estimation", "type": "object", "properties": {} } diff --git a/packages/tasks/src/tasks/document-question-answering/data.ts b/packages/tasks/src/tasks/document-question-answering/data.ts index f36ed212c..d40cfe613 100644 --- a/packages/tasks/src/tasks/document-question-answering/data.ts +++ b/packages/tasks/src/tasks/document-question-answering/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/document-question-answering/inference.ts b/packages/tasks/src/tasks/document-question-answering/inference.ts index 1636dce9d..8ec6b58a0 100644 --- a/packages/tasks/src/tasks/document-question-answering/inference.ts +++ b/packages/tasks/src/tasks/document-question-answering/inference.ts @@ -12,7 +12,7 @@ export interface DocumentQuestionAnsweringInput { */ inputs: DocumentQuestionAnsweringInputData; /** - * Additional inference parameters + * Additional inference parameters for Document Question Answering */ parameters?: DocumentQuestionAnsweringParameters; [property: string]: unknown; @@ -32,8 +32,6 @@ export interface DocumentQuestionAnsweringInputData { [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Document Question Answering */ export interface DocumentQuestionAnsweringParameters { @@ -102,9 +100,5 @@ export interface DocumentQuestionAnsweringOutputElement { * boxes). */ start: number; - /** - * The index of each word/box pair that is in the answer - */ - words: number[]; [property: string]: unknown; } diff --git a/packages/tasks/src/tasks/document-question-answering/spec/input.json b/packages/tasks/src/tasks/document-question-answering/spec/input.json index b017ce469..e04e53436 100644 --- a/packages/tasks/src/tasks/document-question-answering/spec/input.json +++ b/packages/tasks/src/tasks/document-question-answering/spec/input.json @@ -21,14 +21,13 @@ "required": ["image", "question"] }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Document Question Answering", "$ref": "#/$defs/DocumentQuestionAnsweringParameters" } }, "$defs": { "DocumentQuestionAnsweringParameters": { "title": "DocumentQuestionAnsweringParameters", - "description": "Additional inference parameters for Document Question Answering", "type": "object", "properties": { "doc_stride": { diff --git a/packages/tasks/src/tasks/document-question-answering/spec/output.json b/packages/tasks/src/tasks/document-question-answering/spec/output.json index 4fda3771a..f8d0c20fe 100644 --- a/packages/tasks/src/tasks/document-question-answering/spec/output.json +++ b/packages/tasks/src/tasks/document-question-answering/spec/output.json @@ -22,15 +22,8 @@ "end": { "type": "integer", "description": "The end word index of the answer (in the OCR\u2019d version of the input or provided word boxes)." - }, - "words": { - "type": "array", - "items": { - "type": "integer" - }, - "description": "The index of each word/box pair that is in the answer" } }, - "required": ["answer", "score", "start", "end", "words"] + "required": ["answer", "score", "start", "end"] } } diff --git a/packages/tasks/src/tasks/feature-extraction/data.ts b/packages/tasks/src/tasks/feature-extraction/data.ts index 8bf923349..75f5d6f5e 100644 --- a/packages/tasks/src/tasks/feature-extraction/data.ts +++ b/packages/tasks/src/tasks/feature-extraction/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/feature-extraction/inference.ts b/packages/tasks/src/tasks/feature-extraction/inference.ts index 96194d7e9..404b10308 100644 --- a/packages/tasks/src/tasks/feature-extraction/inference.ts +++ b/packages/tasks/src/tasks/feature-extraction/inference.ts @@ -23,7 +23,7 @@ export interface FeatureExtractionInput { * The name of the prompt that should be used by for encoding. If not set, no prompt * will be applied. * - * Must be a key in the `Sentence Transformers` configuration `prompts` dictionary. + * Must be a key in the `sentence-transformers` configuration `prompts` dictionary. * * For example if ``prompt_name`` is "query" and the ``prompts`` is {"query": "query: ", * ...}, diff --git a/packages/tasks/src/tasks/feature-extraction/spec/input.json b/packages/tasks/src/tasks/feature-extraction/spec/input.json index 94e8d7a0b..a4fec711a 100644 --- a/packages/tasks/src/tasks/feature-extraction/spec/input.json +++ b/packages/tasks/src/tasks/feature-extraction/spec/input.json @@ -17,7 +17,7 @@ }, "prompt_name": { "type": "string", - "description": "The name of the prompt that should be used by for encoding. If not set, no prompt\nwill be applied.\n\nMust be a key in the `Sentence Transformers` configuration `prompts` dictionary.\n\nFor example if ``prompt_name`` is \"query\" and the ``prompts`` is {\"query\": \"query: \", ...},\nthen the sentence \"What is the capital of France?\" will be encoded as\n\"query: What is the capital of France?\" because the prompt text will be prepended before\nany text to encode.", + "description": "The name of the prompt that should be used by for encoding. If not set, no prompt\nwill be applied.\n\nMust be a key in the `sentence-transformers` configuration `prompts` dictionary.\n\nFor example if ``prompt_name`` is \"query\" and the ``prompts`` is {\"query\": \"query: \", ...},\nthen the sentence \"What is the capital of France?\" will be encoded as\n\"query: What is the capital of France?\" because the prompt text will be prepended before\nany text to encode.", "default": "null", "example": "null", "nullable": true diff --git a/packages/tasks/src/tasks/fill-mask/data.ts b/packages/tasks/src/tasks/fill-mask/data.ts index 55550f042..1ed7849f1 100644 --- a/packages/tasks/src/tasks/fill-mask/data.ts +++ b/packages/tasks/src/tasks/fill-mask/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/fill-mask/inference.ts b/packages/tasks/src/tasks/fill-mask/inference.ts index 4d78ecd81..4f48bb55e 100644 --- a/packages/tasks/src/tasks/fill-mask/inference.ts +++ b/packages/tasks/src/tasks/fill-mask/inference.ts @@ -12,14 +12,12 @@ export interface FillMaskInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Fill Mask */ parameters?: FillMaskParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Fill Mask */ export interface FillMaskParameters { diff --git a/packages/tasks/src/tasks/fill-mask/spec/input.json b/packages/tasks/src/tasks/fill-mask/spec/input.json index cd3271e4a..a2ebaf13e 100644 --- a/packages/tasks/src/tasks/fill-mask/spec/input.json +++ b/packages/tasks/src/tasks/fill-mask/spec/input.json @@ -10,14 +10,13 @@ "type": "string" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Fill Mask", "$ref": "#/$defs/FillMaskParameters" } }, "$defs": { "FillMaskParameters": { "title": "FillMaskParameters", - "description": "Additional inference parameters for Fill Mask", "type": "object", "properties": { "top_k": { diff --git a/packages/tasks/src/tasks/image-classification/data.ts b/packages/tasks/src/tasks/image-classification/data.ts index 875b19742..a4cd9fd42 100644 --- a/packages/tasks/src/tasks/image-classification/data.ts +++ b/packages/tasks/src/tasks/image-classification/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/image-classification/inference.ts b/packages/tasks/src/tasks/image-classification/inference.ts index bd56a7d31..1f6fd103e 100644 --- a/packages/tasks/src/tasks/image-classification/inference.ts +++ b/packages/tasks/src/tasks/image-classification/inference.ts @@ -13,17 +13,18 @@ export interface ImageClassificationInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Image Classification */ parameters?: ImageClassificationParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Image Classification */ export interface ImageClassificationParameters { + /** + * The function to apply to the model outputs in order to retrieve the scores. + */ function_to_apply?: ClassificationOutputTransform; /** * When specified, limits the output to the top K most probable classes. diff --git a/packages/tasks/src/tasks/image-classification/spec/input.json b/packages/tasks/src/tasks/image-classification/spec/input.json index cf0b30ec5..3e2bd13d4 100644 --- a/packages/tasks/src/tasks/image-classification/spec/input.json +++ b/packages/tasks/src/tasks/image-classification/spec/input.json @@ -10,19 +10,19 @@ "description": "The input image data as a base64-encoded string. If no `parameters` are provided, you can also provide the image data as a raw bytes payload." }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Image Classification", "$ref": "#/$defs/ImageClassificationParameters" } }, "$defs": { "ImageClassificationParameters": { "title": "ImageClassificationParameters", - "description": "Additional inference parameters for Image Classification", "type": "object", "properties": { "function_to_apply": { "title": "ImageClassificationOutputTransform", - "$ref": "/inference/schemas/common-definitions.json#/definitions/ClassificationOutputTransform" + "$ref": "/inference/schemas/common-definitions.json#/definitions/ClassificationOutputTransform", + "description": "The function to apply to the model outputs in order to retrieve the scores." }, "top_k": { "type": "integer", diff --git a/packages/tasks/src/tasks/image-feature-extraction/data.ts b/packages/tasks/src/tasks/image-feature-extraction/data.ts index 71861204e..8880d5bf8 100644 --- a/packages/tasks/src/tasks/image-feature-extraction/data.ts +++ b/packages/tasks/src/tasks/image-feature-extraction/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/image-segmentation/data.ts b/packages/tasks/src/tasks/image-segmentation/data.ts index edbd100ea..555e5db3e 100644 --- a/packages/tasks/src/tasks/image-segmentation/data.ts +++ b/packages/tasks/src/tasks/image-segmentation/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ @@ -44,8 +44,7 @@ const taskData: TaskDataCustom = { models: [ { // TO DO: write description - description: - "Solid semantic segmentation model trained on ADE20k.", + description: "Solid semantic segmentation model trained on ADE20k.", id: "openmmlab/upernet-convnext-small", }, { diff --git a/packages/tasks/src/tasks/image-segmentation/inference.ts b/packages/tasks/src/tasks/image-segmentation/inference.ts index 4ccd36e41..e299d0e67 100644 --- a/packages/tasks/src/tasks/image-segmentation/inference.ts +++ b/packages/tasks/src/tasks/image-segmentation/inference.ts @@ -13,14 +13,12 @@ export interface ImageSegmentationInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Image Segmentation */ parameters?: ImageSegmentationParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Image Segmentation */ export interface ImageSegmentationParameters { diff --git a/packages/tasks/src/tasks/image-segmentation/spec/input.json b/packages/tasks/src/tasks/image-segmentation/spec/input.json index 697f8959b..2023f93f1 100644 --- a/packages/tasks/src/tasks/image-segmentation/spec/input.json +++ b/packages/tasks/src/tasks/image-segmentation/spec/input.json @@ -10,14 +10,13 @@ "description": "The input image data as a base64-encoded string. If no `parameters` are provided, you can also provide the image data as a raw bytes payload." }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Image Segmentation", "$ref": "#/$defs/ImageSegmentationParameters" } }, "$defs": { "ImageSegmentationParameters": { "title": "ImageSegmentationParameters", - "description": "Additional inference parameters for Image Segmentation", "type": "object", "properties": { "mask_threshold": { diff --git a/packages/tasks/src/tasks/image-text-to-text/about.md b/packages/tasks/src/tasks/image-text-to-text/about.md index f220604fc..8da1621bb 100644 --- a/packages/tasks/src/tasks/image-text-to-text/about.md +++ b/packages/tasks/src/tasks/image-text-to-text/about.md @@ -32,39 +32,51 @@ Vision language models can recognize images through descriptions. When given det ## Inference -You can use the Transformers library to interact with vision-language models. You can load the model like below. +You can use the Transformers library to interact with [vision-language models](https://huggingface.co/models?pipeline_tag=image-text-to-text&transformers). Specifically, `pipeline` makes it easy to infer models. + +Initialize the pipeline first. + +```python +from transformers import pipeline + +pipe = pipeline("image-text-to-text", model="llava-hf/llava-interleave-qwen-0.5b-hf") +``` + +The model's built-in chat template will be used to format the conversational input. We can pass the image as an URL in the `content` part of the user message: ```python -from transformers import LlavaNextProcessor, LlavaNextForConditionalGeneration -import torch - -device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') -processor = LlavaNextProcessor.from_pretrained("llava-hf/llava-v1.6-mistral-7b-hf") -model = LlavaNextForConditionalGeneration.from_pretrained( - "llava-hf/llava-v1.6-mistral-7b-hf", - torch_dtype=torch.float16 -) -model.to(device) +messages = [ + { + "role": "user", + "content": [ + { + "type": "image", + "image": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/bee.jpg", + }, + {"type": "text", "text": "Describe this image."}, + ], + } + ] + ``` -We can infer by passing image and text dialogues. +We can now directly pass in the messages to the pipeline to infer. The `return_full_text` flag is used to return the full prompt in the response, including the user input. Here we pass `False` to only return the generated text. ```python -from PIL import Image -import requests +outputs = pipe(text=messages, max_new_tokens=60, return_full_text=False) -# image of a radar chart -url = "https://github.com/haotian-liu/LLaVA/blob/1a91fc274d7c35a9b50b3cb29c4247ae5837ce39/images/llava_v1_5_radar.jpg?raw=true" -image = Image.open(requests.get(url, stream=True).raw) -prompt = "[INST] \nWhat is shown in this image? [/INST]" +outputs[0]["generated_text"] +# The image captures a moment of tranquility in nature. At the center of the frame, a pink flower with a yellow center is in full bloom. The flower is surrounded by a cluster of red flowers, their vibrant color contrasting with the pink of the flower. \n\nA black and yellow bee is per +``` -inputs = processor(prompt, image, return_tensors="pt").to(device) -output = model.generate(**inputs, max_new_tokens=100) +You can also use the Inference API to test image-text-to-text models. You need to use a [Hugging Face token](https://huggingface.co/settings/tokens) for authentication. -print(processor.decode(output[0], skip_special_tokens=True)) -# The image appears to be a radar chart, which is a type of multivariate chart that displays values for multiple variables represented on axes -# starting from the same point. This particular radar chart is showing the performance of different models or systems across various metrics. -# The axes represent different metrics or benchmarks, such as MM-Vet, MM-Vet, MM-Vet, MM-Vet, MM-Vet, MM-V +```bash +curl https://api-inference.huggingface.co/models/meta-llama/Llama-3.2-11B-Vision-Instruct \ + -X POST \ + -d '{"messages": [{"role": "user","content": [{"type": "image"}, {"type": "text", "text": "Can you describe the image?"}]}]}' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer hf_***" ``` ## Useful Resources diff --git a/packages/tasks/src/tasks/image-text-to-text/data.ts b/packages/tasks/src/tasks/image-text-to-text/data.ts index e096f0195..e77ffc68f 100644 --- a/packages/tasks/src/tasks/image-text-to-text/data.ts +++ b/packages/tasks/src/tasks/image-text-to-text/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/image-to-3d/data.ts b/packages/tasks/src/tasks/image-to-3d/data.ts index e9dd93651..6556e1b02 100644 --- a/packages/tasks/src/tasks/image-to-3d/data.ts +++ b/packages/tasks/src/tasks/image-to-3d/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/image-to-image/data.ts b/packages/tasks/src/tasks/image-to-image/data.ts index a08c19edd..fa2ddc248 100644 --- a/packages/tasks/src/tasks/image-to-image/data.ts +++ b/packages/tasks/src/tasks/image-to-image/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/image-to-image/inference.ts b/packages/tasks/src/tasks/image-to-image/inference.ts index 8ba34b5ff..658bbd012 100644 --- a/packages/tasks/src/tasks/image-to-image/inference.ts +++ b/packages/tasks/src/tasks/image-to-image/inference.ts @@ -14,15 +14,13 @@ export interface ImageToImageInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Image To Image */ parameters?: ImageToImageParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Image To Image */ export interface ImageToImageParameters { diff --git a/packages/tasks/src/tasks/image-to-image/spec/input.json b/packages/tasks/src/tasks/image-to-image/spec/input.json index 23695c6b1..5020d84cf 100644 --- a/packages/tasks/src/tasks/image-to-image/spec/input.json +++ b/packages/tasks/src/tasks/image-to-image/spec/input.json @@ -10,14 +10,13 @@ "description": "The input image data as a base64-encoded string. If no `parameters` are provided, you can also provide the image data as a raw bytes payload." }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Image To Image", "$ref": "#/$defs/ImageToImageParameters" } }, "$defs": { "ImageToImageParameters": { "title": "ImageToImageParameters", - "description": "Additional inference parameters for Image To Image", "type": "object", "properties": { "guidance_scale": { diff --git a/packages/tasks/src/tasks/image-to-text/data.ts b/packages/tasks/src/tasks/image-to-text/data.ts index 64f4fe5c8..a269d9497 100644 --- a/packages/tasks/src/tasks/image-to-text/data.ts +++ b/packages/tasks/src/tasks/image-to-text/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/image-to-text/inference.ts b/packages/tasks/src/tasks/image-to-text/inference.ts index 44a95f1f5..fd81b4e62 100644 --- a/packages/tasks/src/tasks/image-to-text/inference.ts +++ b/packages/tasks/src/tasks/image-to-text/inference.ts @@ -13,15 +13,13 @@ export interface ImageToTextInput { */ inputs: unknown; /** - * Additional inference parameters + * Additional inference parameters for Image To Text */ parameters?: ImageToTextParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Image To Text */ export interface ImageToTextParameters { @@ -38,8 +36,6 @@ export interface ImageToTextParameters { /** * Parametrization of the text generation process - * - * Ad-hoc parametrization of the text generation process */ export interface GenerationParameters { /** diff --git a/packages/tasks/src/tasks/image-to-text/spec/input.json b/packages/tasks/src/tasks/image-to-text/spec/input.json index 175868614..7b3fd2756 100644 --- a/packages/tasks/src/tasks/image-to-text/spec/input.json +++ b/packages/tasks/src/tasks/image-to-text/spec/input.json @@ -9,14 +9,13 @@ "description": "The input image data" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Image To Text", "$ref": "#/$defs/ImageToTextParameters" } }, "$defs": { "ImageToTextParameters": { "title": "ImageToTextParameters", - "description": "Additional inference parameters for Image To Text", "type": "object", "properties": { "max_new_tokens": { diff --git a/packages/tasks/src/tasks/index.ts b/packages/tasks/src/tasks/index.ts index 3e9a02663..c1df8116d 100644 --- a/packages/tasks/src/tasks/index.ts +++ b/packages/tasks/src/tasks/index.ts @@ -1,49 +1,49 @@ -import type { PipelineType } from "../pipelines"; -import { PIPELINE_DATA } from "../pipelines"; +import type { PipelineType } from "../pipelines.js"; +import { PIPELINE_DATA } from "../pipelines.js"; -import audioClassification from "./audio-classification/data"; -import audioToAudio from "./audio-to-audio/data"; -import automaticSpeechRecognition from "./automatic-speech-recognition/data"; -import documentQuestionAnswering from "./document-question-answering/data"; -import featureExtraction from "./feature-extraction/data"; -import fillMask from "./fill-mask/data"; -import imageClassification from "./image-classification/data"; -import imageFeatureExtraction from "./image-feature-extraction/data"; -import imageToImage from "./image-to-image/data"; -import imageToText from "./image-to-text/data"; -import imageTextToText from "./image-text-to-text/data"; -import imageSegmentation from "./image-segmentation/data"; -import maskGeneration from "./mask-generation/data"; -import objectDetection from "./object-detection/data"; -import depthEstimation from "./depth-estimation/data"; -import placeholder from "./placeholder/data"; -import reinforcementLearning from "./reinforcement-learning/data"; -import questionAnswering from "./question-answering/data"; -import sentenceSimilarity from "./sentence-similarity/data"; -import summarization from "./summarization/data"; -import tableQuestionAnswering from "./table-question-answering/data"; -import tabularClassification from "./tabular-classification/data"; -import tabularRegression from "./tabular-regression/data"; -import textToImage from "./text-to-image/data"; -import textToSpeech from "./text-to-speech/data"; -import tokenClassification from "./token-classification/data"; -import translation from "./translation/data"; -import textClassification from "./text-classification/data"; -import textGeneration from "./text-generation/data"; -import textToVideo from "./text-to-video/data"; -import unconditionalImageGeneration from "./unconditional-image-generation/data"; -import videoClassification from "./video-classification/data"; -import visualQuestionAnswering from "./visual-question-answering/data"; -import zeroShotClassification from "./zero-shot-classification/data"; -import zeroShotImageClassification from "./zero-shot-image-classification/data"; -import zeroShotObjectDetection from "./zero-shot-object-detection/data"; -import imageTo3D from "./image-to-3d/data"; -import textTo3D from "./text-to-3d/data"; -import keypointDetection from "./keypoint-detection/data"; -import videoTextToText from "./video-text-to-text/data"; +import audioClassification from "./audio-classification/data.js"; +import audioToAudio from "./audio-to-audio/data.js"; +import automaticSpeechRecognition from "./automatic-speech-recognition/data.js"; +import documentQuestionAnswering from "./document-question-answering/data.js"; +import featureExtraction from "./feature-extraction/data.js"; +import fillMask from "./fill-mask/data.js"; +import imageClassification from "./image-classification/data.js"; +import imageFeatureExtraction from "./image-feature-extraction/data.js"; +import imageToImage from "./image-to-image/data.js"; +import imageToText from "./image-to-text/data.js"; +import imageTextToText from "./image-text-to-text/data.js"; +import imageSegmentation from "./image-segmentation/data.js"; +import maskGeneration from "./mask-generation/data.js"; +import objectDetection from "./object-detection/data.js"; +import depthEstimation from "./depth-estimation/data.js"; +import placeholder from "./placeholder/data.js"; +import reinforcementLearning from "./reinforcement-learning/data.js"; +import questionAnswering from "./question-answering/data.js"; +import sentenceSimilarity from "./sentence-similarity/data.js"; +import summarization from "./summarization/data.js"; +import tableQuestionAnswering from "./table-question-answering/data.js"; +import tabularClassification from "./tabular-classification/data.js"; +import tabularRegression from "./tabular-regression/data.js"; +import textToImage from "./text-to-image/data.js"; +import textToSpeech from "./text-to-speech/data.js"; +import tokenClassification from "./token-classification/data.js"; +import translation from "./translation/data.js"; +import textClassification from "./text-classification/data.js"; +import textGeneration from "./text-generation/data.js"; +import textToVideo from "./text-to-video/data.js"; +import unconditionalImageGeneration from "./unconditional-image-generation/data.js"; +import videoClassification from "./video-classification/data.js"; +import visualQuestionAnswering from "./visual-question-answering/data.js"; +import zeroShotClassification from "./zero-shot-classification/data.js"; +import zeroShotImageClassification from "./zero-shot-image-classification/data.js"; +import zeroShotObjectDetection from "./zero-shot-object-detection/data.js"; +import imageTo3D from "./image-to-3d/data.js"; +import textTo3D from "./text-to-3d/data.js"; +import keypointDetection from "./keypoint-detection/data.js"; +import videoTextToText from "./video-text-to-text/data.js"; -export type * from "./audio-classification/inference"; -export type * from "./automatic-speech-recognition/inference"; +export type * from "./audio-classification/inference.js"; +export type * from "./automatic-speech-recognition/inference.js"; export type { ChatCompletionInput, ChatCompletionInputMessage, @@ -53,36 +53,36 @@ export type { ChatCompletionStreamOutput, ChatCompletionStreamOutputChoice, ChatCompletionStreamOutputDelta, -} from "./chat-completion/inference"; -export type * from "./document-question-answering/inference"; -export type * from "./feature-extraction/inference"; -export type * from "./fill-mask/inference"; +} from "./chat-completion/inference.js"; +export type * from "./document-question-answering/inference.js"; +export type * from "./feature-extraction/inference.js"; +export type * from "./fill-mask/inference.js"; export type { ImageClassificationInput, ImageClassificationOutput, ImageClassificationOutputElement, ImageClassificationParameters, -} from "./image-classification/inference"; -export type * from "./image-to-image/inference"; -export type { ImageToTextInput, ImageToTextOutput, ImageToTextParameters } from "./image-to-text/inference"; -export type * from "./image-segmentation/inference"; -export type * from "./object-detection/inference"; -export type * from "./depth-estimation/inference"; -export type * from "./question-answering/inference"; -export type * from "./sentence-similarity/inference"; -export type * from "./summarization/inference"; -export type * from "./table-question-answering/inference"; -export type { TextToImageInput, TextToImageOutput, TextToImageParameters } from "./text-to-image/inference"; -export type { TextToSpeechParameters, TextToSpeechInput, TextToSpeechOutput } from "./text-to-speech/inference"; -export type * from "./token-classification/inference"; -export type { TranslationInput, TranslationOutput } from "./translation/inference"; +} from "./image-classification/inference.js"; +export type * from "./image-to-image/inference.js"; +export type { ImageToTextInput, ImageToTextOutput, ImageToTextParameters } from "./image-to-text/inference.js"; +export type * from "./image-segmentation/inference.js"; +export type * from "./object-detection/inference.js"; +export type * from "./depth-estimation/inference.js"; +export type * from "./question-answering/inference.js"; +export type * from "./sentence-similarity/inference.js"; +export type * from "./summarization/inference.js"; +export type * from "./table-question-answering/inference.js"; +export type { TextToImageInput, TextToImageOutput, TextToImageParameters } from "./text-to-image/inference.js"; +export type { TextToSpeechParameters, TextToSpeechInput, TextToSpeechOutput } from "./text-to-speech/inference.js"; +export type * from "./token-classification/inference.js"; +export type { TranslationInput, TranslationOutput } from "./translation/inference.js"; export type { ClassificationOutputTransform, TextClassificationInput, TextClassificationOutput, TextClassificationOutputElement, TextClassificationParameters, -} from "./text-classification/inference"; +} from "./text-classification/inference.js"; export type { TextGenerationOutputFinishReason, TextGenerationOutputPrefillToken, @@ -94,20 +94,19 @@ export type { TextGenerationOutputToken, TextGenerationStreamOutputStreamDetails, TextGenerationStreamOutput, -} from "./text-generation/inference"; -export type * from "./video-classification/inference"; -export type * from "./visual-question-answering/inference"; -export type * from "./zero-shot-classification/inference"; -export type * from "./zero-shot-image-classification/inference"; +} from "./text-generation/inference.js"; +export type * from "./video-classification/inference.js"; +export type * from "./visual-question-answering/inference.js"; +export type * from "./zero-shot-classification/inference.js"; +export type * from "./zero-shot-image-classification/inference.js"; export type { BoundingBox, ZeroShotObjectDetectionInput, - ZeroShotObjectDetectionInputData, ZeroShotObjectDetectionOutput, ZeroShotObjectDetectionOutputElement, -} from "./zero-shot-object-detection/inference"; +} from "./zero-shot-object-detection/inference.js"; -import type { ModelLibraryKey } from "../model-libraries"; +import type { ModelLibraryKey } from "../model-libraries.js"; /** * Model libraries compatible with each ML task @@ -116,6 +115,7 @@ export const TASKS_MODEL_LIBRARIES: Record = { "audio-classification": ["speechbrain", "transformers", "transformers.js"], "audio-to-audio": ["asteroid", "fairseq", "speechbrain"], "automatic-speech-recognition": ["espnet", "nemo", "speechbrain", "transformers", "transformers.js"], + "audio-text-to-text": [], "depth-estimation": ["transformers", "transformers.js"], "document-question-answering": ["transformers", "transformers.js"], "feature-extraction": ["sentence-transformers", "transformers", "transformers.js"], @@ -197,6 +197,7 @@ export const TASKS_DATA: Record = { "any-to-any": getData("any-to-any", placeholder), "audio-classification": getData("audio-classification", audioClassification), "audio-to-audio": getData("audio-to-audio", audioToAudio), + "audio-text-to-text": getData("audio-text-to-text", placeholder), "automatic-speech-recognition": getData("automatic-speech-recognition", automaticSpeechRecognition), "depth-estimation": getData("depth-estimation", depthEstimation), "document-question-answering": getData("document-question-answering", documentQuestionAnswering), diff --git a/packages/tasks/src/tasks/keypoint-detection/data.ts b/packages/tasks/src/tasks/keypoint-detection/data.ts index 194e48d48..e65b805ba 100644 --- a/packages/tasks/src/tasks/keypoint-detection/data.ts +++ b/packages/tasks/src/tasks/keypoint-detection/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/mask-generation/about.md b/packages/tasks/src/tasks/mask-generation/about.md index 18d6d38db..2fa3a65d2 100644 --- a/packages/tasks/src/tasks/mask-generation/about.md +++ b/packages/tasks/src/tasks/mask-generation/about.md @@ -12,6 +12,16 @@ Generating masks can facilitate learning, especially in semi or unsupervised lea For applications where humans are in the loop, masks highlight certain regions of images for humans to validate. +### Medical Imaging + +Mask generation models are used in medical imaging to aid in segmenting and analyzing specific regions. + +### Autonomous Vehicles + +Mask generation models are used to create segments and masks for obstacles and other objects in view. + +This page was made possible thanks to the efforts of [Raj Aryan](https://huggingface.co/thatrajaryan) and other contributors. + ## Task Variants ### Segmentation diff --git a/packages/tasks/src/tasks/mask-generation/data.ts b/packages/tasks/src/tasks/mask-generation/data.ts index 6b18b2dbf..cc18380db 100644 --- a/packages/tasks/src/tasks/mask-generation/data.ts +++ b/packages/tasks/src/tasks/mask-generation/data.ts @@ -1,7 +1,16 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { - datasets: [], + datasets: [ + { + description: "Widely used benchmark dataset for multiple Vision tasks.", + id: "merve/coco2017", + }, + { + description: "Medical Imaging dataset of the Human Brain for segmentation and mask generating tasks", + id: "rocky93/BraTS_segmentation", + }, + ], demo: { inputs: [ { @@ -16,7 +25,12 @@ const taskData: TaskDataCustom = { }, ], }, - metrics: [], + metrics: [ + { + description: "IoU is used to measure the overlap between predicted mask and the ground truth mask.", + id: "Intersection over Union (IoU)", + }, + ], models: [ { description: "Small yet powerful mask generation model.", diff --git a/packages/tasks/src/tasks/object-detection/data.ts b/packages/tasks/src/tasks/object-detection/data.ts index 80f87a590..2b1a8a4a9 100644 --- a/packages/tasks/src/tasks/object-detection/data.ts +++ b/packages/tasks/src/tasks/object-detection/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ @@ -51,8 +51,7 @@ const taskData: TaskDataCustom = { id: "jameslahm/yolov10x", }, { - description: - "Fast and accurate object detection model trained on COCO and Object365 datasets.", + description: "Fast and accurate object detection model trained on COCO and Object365 datasets.", id: "PekingU/rtdetr_r18vd_coco_o365", }, ], diff --git a/packages/tasks/src/tasks/object-detection/inference.ts b/packages/tasks/src/tasks/object-detection/inference.ts index d117dcb0b..4bb02bd01 100644 --- a/packages/tasks/src/tasks/object-detection/inference.ts +++ b/packages/tasks/src/tasks/object-detection/inference.ts @@ -13,14 +13,12 @@ export interface ObjectDetectionInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Object Detection */ parameters?: ObjectDetectionParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Object Detection */ export interface ObjectDetectionParameters { diff --git a/packages/tasks/src/tasks/object-detection/spec/input.json b/packages/tasks/src/tasks/object-detection/spec/input.json index d00deefec..948392bf0 100644 --- a/packages/tasks/src/tasks/object-detection/spec/input.json +++ b/packages/tasks/src/tasks/object-detection/spec/input.json @@ -10,14 +10,13 @@ "description": "The input image data as a base64-encoded string. If no `parameters` are provided, you can also provide the image data as a raw bytes payload." }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Object Detection", "$ref": "#/$defs/ObjectDetectionParameters" } }, "$defs": { "ObjectDetectionParameters": { "title": "ObjectDetectionParameters", - "description": "Additional inference parameters for Object Detection", "type": "object", "properties": { "threshold": { diff --git a/packages/tasks/src/tasks/placeholder/data.ts b/packages/tasks/src/tasks/placeholder/data.ts index 110b43703..5fc9bbb8f 100644 --- a/packages/tasks/src/tasks/placeholder/data.ts +++ b/packages/tasks/src/tasks/placeholder/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [], diff --git a/packages/tasks/src/tasks/placeholder/spec/input.json b/packages/tasks/src/tasks/placeholder/spec/input.json index d31f4aac6..aab9dd152 100644 --- a/packages/tasks/src/tasks/placeholder/spec/input.json +++ b/packages/tasks/src/tasks/placeholder/spec/input.json @@ -10,14 +10,13 @@ "type": "string" }, "parameters": { - "description": "Additional inference parameters", + "description": "TODO: describe additional parameters here.", "$ref": "#/$defs/Parameters" } }, "$defs": { "Parameters": { "title": "Parameters", - "description": "TODO: describe additional parameters here.", "type": "object", "properties": { "dummy_parameter_name": { diff --git a/packages/tasks/src/tasks/question-answering/data.ts b/packages/tasks/src/tasks/question-answering/data.ts index ac1443adf..cf75dd3d7 100644 --- a/packages/tasks/src/tasks/question-answering/data.ts +++ b/packages/tasks/src/tasks/question-answering/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/question-answering/inference.ts b/packages/tasks/src/tasks/question-answering/inference.ts index eaef8dfe3..bd478276b 100644 --- a/packages/tasks/src/tasks/question-answering/inference.ts +++ b/packages/tasks/src/tasks/question-answering/inference.ts @@ -12,7 +12,7 @@ export interface QuestionAnsweringInput { */ inputs: QuestionAnsweringInputData; /** - * Additional inference parameters + * Additional inference parameters for Question Answering */ parameters?: QuestionAnsweringParameters; [property: string]: unknown; @@ -32,8 +32,6 @@ export interface QuestionAnsweringInputData { [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Question Answering */ export interface QuestionAnsweringParameters { diff --git a/packages/tasks/src/tasks/question-answering/spec/input.json b/packages/tasks/src/tasks/question-answering/spec/input.json index 70d5607cf..266b329df 100644 --- a/packages/tasks/src/tasks/question-answering/spec/input.json +++ b/packages/tasks/src/tasks/question-answering/spec/input.json @@ -22,14 +22,13 @@ "required": ["question", "context"] }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Question Answering", "$ref": "#/$defs/QuestionAnsweringParameters" } }, "$defs": { "QuestionAnsweringParameters": { "title": "QuestionAnsweringParameters", - "description": "Additional inference parameters for Question Answering", "type": "object", "properties": { "top_k": { diff --git a/packages/tasks/src/tasks/reinforcement-learning/data.ts b/packages/tasks/src/tasks/reinforcement-learning/data.ts index 71290d677..231ecabda 100644 --- a/packages/tasks/src/tasks/reinforcement-learning/data.ts +++ b/packages/tasks/src/tasks/reinforcement-learning/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/sentence-similarity/data.ts b/packages/tasks/src/tasks/sentence-similarity/data.ts index 3ef54bbd3..ff6b877a0 100644 --- a/packages/tasks/src/tasks/sentence-similarity/data.ts +++ b/packages/tasks/src/tasks/sentence-similarity/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/sentence-similarity/inference.ts b/packages/tasks/src/tasks/sentence-similarity/inference.ts index 646e18b48..277aa4a83 100644 --- a/packages/tasks/src/tasks/sentence-similarity/inference.ts +++ b/packages/tasks/src/tasks/sentence-similarity/inference.ts @@ -12,7 +12,7 @@ export type SentenceSimilarityOutput = number[]; export interface SentenceSimilarityInput { inputs: SentenceSimilarityInputData; /** - * Additional inference parameters + * Additional inference parameters for Sentence Similarity */ parameters?: { [key: string]: unknown }; [property: string]: unknown; diff --git a/packages/tasks/src/tasks/sentence-similarity/spec/input.json b/packages/tasks/src/tasks/sentence-similarity/spec/input.json index ecff3479d..8958f3833 100644 --- a/packages/tasks/src/tasks/sentence-similarity/spec/input.json +++ b/packages/tasks/src/tasks/sentence-similarity/spec/input.json @@ -24,14 +24,13 @@ "required": ["sourceSentence", "sentences"] }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Sentence Similarity", "$ref": "#/$defs/SentenceSimilarityParameters" } }, "$defs": { "SentenceSimilarityParameters": { "title": "SentenceSimilarityParameters", - "description": "Additional inference parameters for Sentence Similarity", "type": "object", "properties": {} } diff --git a/packages/tasks/src/tasks/summarization/data.ts b/packages/tasks/src/tasks/summarization/data.ts index 239a04fc4..de7eb4e4c 100644 --- a/packages/tasks/src/tasks/summarization/data.ts +++ b/packages/tasks/src/tasks/summarization/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { canonicalId: "text2text-generation", diff --git a/packages/tasks/src/tasks/summarization/inference.ts b/packages/tasks/src/tasks/summarization/inference.ts index a08e25230..ecec9e97e 100644 --- a/packages/tasks/src/tasks/summarization/inference.ts +++ b/packages/tasks/src/tasks/summarization/inference.ts @@ -13,15 +13,13 @@ export interface SummarizationInput { */ inputs: string; /** - * Additional inference parameters. + * Additional inference parameters for summarization. */ parameters?: SummarizationParameters; [property: string]: unknown; } /** - * Additional inference parameters. - * * Additional inference parameters for summarization. */ export interface SummarizationParameters { diff --git a/packages/tasks/src/tasks/summarization/spec/input.json b/packages/tasks/src/tasks/summarization/spec/input.json index d33152857..c2d7aebaf 100644 --- a/packages/tasks/src/tasks/summarization/spec/input.json +++ b/packages/tasks/src/tasks/summarization/spec/input.json @@ -10,14 +10,13 @@ "type": "string" }, "parameters": { - "description": "Additional inference parameters.", + "description": "Additional inference parameters for summarization.", "$ref": "#/$defs/SummarizationParameters" } }, "$defs": { "SummarizationParameters": { "title": "SummarizationParameters", - "description": "Additional inference parameters for summarization.", "type": "object", "properties": { "clean_up_tokenization_spaces": { diff --git a/packages/tasks/src/tasks/table-question-answering/data.ts b/packages/tasks/src/tasks/table-question-answering/data.ts index 7a4691146..b5f161a4c 100644 --- a/packages/tasks/src/tasks/table-question-answering/data.ts +++ b/packages/tasks/src/tasks/table-question-answering/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/table-question-answering/inference.ts b/packages/tasks/src/tasks/table-question-answering/inference.ts index 7e79fa2c8..18339e033 100644 --- a/packages/tasks/src/tasks/table-question-answering/inference.ts +++ b/packages/tasks/src/tasks/table-question-answering/inference.ts @@ -12,11 +12,9 @@ export interface TableQuestionAnsweringInput { */ inputs: TableQuestionAnsweringInputData; /** - * Additional inference parameters + * Additional inference parameters for Table Question Answering */ - parameters?: { - [key: string]: unknown; - }; + parameters?: TableQuestionAnsweringParameters; [property: string]: unknown; } /** @@ -35,6 +33,30 @@ export interface TableQuestionAnsweringInputData { }; [property: string]: unknown; } +/** + * Additional inference parameters for Table Question Answering + */ +export interface TableQuestionAnsweringParameters { + /** + * Activates and controls padding. + */ + padding?: Padding; + /** + * Whether to do inference sequentially or as a batch. Batching is faster, but models like + * SQA require the inference to be done sequentially to extract relations within sequences, + * given their conversational nature. + */ + sequential?: boolean; + /** + * Activates and controls truncation. + */ + truncation?: boolean; + [property: string]: unknown; +} +/** + * Activates and controls padding. + */ +export type Padding = "do_not_pad" | "longest" | "max_length"; export type TableQuestionAnsweringOutput = TableQuestionAnsweringOutputElement[]; /** * Outputs of inference for the Table Question Answering task diff --git a/packages/tasks/src/tasks/table-question-answering/spec/input.json b/packages/tasks/src/tasks/table-question-answering/spec/input.json index 3dfdd02a7..67cc18eae 100644 --- a/packages/tasks/src/tasks/table-question-answering/spec/input.json +++ b/packages/tasks/src/tasks/table-question-answering/spec/input.json @@ -28,16 +28,32 @@ "required": ["table", "question"] }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Table Question Answering", "$ref": "#/$defs/TableQuestionAnsweringParameters" } }, "$defs": { "TableQuestionAnsweringParameters": { "title": "TableQuestionAnsweringParameters", - "description": "Additional inference parameters for Table Question Answering", "type": "object", - "properties": {} + "properties": { + "padding": { + "type": "string", + "default": "do_not_pad", + "description": "Activates and controls padding.", + "enum": ["do_not_pad", "longest", "max_length"] + }, + "sequential": { + "type": "boolean", + "default": "false", + "description": "Whether to do inference sequentially or as a batch. Batching is faster, but models like SQA require the inference to be done sequentially to extract relations within sequences, given their conversational nature." + }, + "truncation": { + "type": "boolean", + "default": "false", + "description": "Activates and controls truncation." + } + } } }, "required": ["inputs"] diff --git a/packages/tasks/src/tasks/tabular-classification/data.ts b/packages/tasks/src/tasks/tabular-classification/data.ts index c7284cc50..80dbe57d2 100644 --- a/packages/tasks/src/tasks/tabular-classification/data.ts +++ b/packages/tasks/src/tasks/tabular-classification/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/tabular-regression/data.ts b/packages/tasks/src/tasks/tabular-regression/data.ts index d4f085d24..c4c0b6d01 100644 --- a/packages/tasks/src/tasks/tabular-regression/data.ts +++ b/packages/tasks/src/tasks/tabular-regression/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/text-classification/data.ts b/packages/tasks/src/tasks/text-classification/data.ts index b6a26dcc4..5ba1506e9 100644 --- a/packages/tasks/src/tasks/text-classification/data.ts +++ b/packages/tasks/src/tasks/text-classification/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/text-classification/inference.ts b/packages/tasks/src/tasks/text-classification/inference.ts index dc9136902..84d6a80fe 100644 --- a/packages/tasks/src/tasks/text-classification/inference.ts +++ b/packages/tasks/src/tasks/text-classification/inference.ts @@ -12,17 +12,18 @@ export interface TextClassificationInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Text Classification */ parameters?: TextClassificationParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Text Classification */ export interface TextClassificationParameters { + /** + * The function to apply to the model outputs in order to retrieve the scores. + */ function_to_apply?: ClassificationOutputTransform; /** * When specified, limits the output to the top K most probable classes. diff --git a/packages/tasks/src/tasks/text-classification/spec/input.json b/packages/tasks/src/tasks/text-classification/spec/input.json index 3bfdeaf6b..468f865dd 100644 --- a/packages/tasks/src/tasks/text-classification/spec/input.json +++ b/packages/tasks/src/tasks/text-classification/spec/input.json @@ -10,19 +10,19 @@ "type": "string" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Text Classification", "$ref": "#/$defs/TextClassificationParameters" } }, "$defs": { "TextClassificationParameters": { "title": "TextClassificationParameters", - "description": "Additional inference parameters for Text Classification", "type": "object", "properties": { "function_to_apply": { "title": "TextClassificationOutputTransform", - "$ref": "/inference/schemas/common-definitions.json#/definitions/ClassificationOutputTransform" + "$ref": "/inference/schemas/common-definitions.json#/definitions/ClassificationOutputTransform", + "description": "The function to apply to the model outputs in order to retrieve the scores." }, "top_k": { "type": "integer", diff --git a/packages/tasks/src/tasks/text-generation/data.ts b/packages/tasks/src/tasks/text-generation/data.ts index ce2240bde..3b783161f 100644 --- a/packages/tasks/src/tasks/text-generation/data.ts +++ b/packages/tasks/src/tasks/text-generation/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/text-generation/spec/input.json b/packages/tasks/src/tasks/text-generation/spec/input.json index 108d9fb3c..0d160419a 100644 --- a/packages/tasks/src/tasks/text-generation/spec/input.json +++ b/packages/tasks/src/tasks/text-generation/spec/input.json @@ -76,7 +76,7 @@ "type": "integer", "format": "int32", "description": "Maximum number of tokens to generate.", - "default": "100", + "default": "1024", "example": "20", "nullable": true, "minimum": 0 diff --git a/packages/tasks/src/tasks/text-to-3d/data.ts b/packages/tasks/src/tasks/text-to-3d/data.ts index e66a21275..30f3aefc4 100644 --- a/packages/tasks/src/tasks/text-to-3d/data.ts +++ b/packages/tasks/src/tasks/text-to-3d/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/text-to-audio/inference.ts b/packages/tasks/src/tasks/text-to-audio/inference.ts index e1105b82e..f08aa87e1 100644 --- a/packages/tasks/src/tasks/text-to-audio/inference.ts +++ b/packages/tasks/src/tasks/text-to-audio/inference.ts @@ -13,15 +13,13 @@ export interface TextToAudioInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Text To Audio */ parameters?: TextToAudioParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Text To Audio */ export interface TextToAudioParameters { @@ -34,8 +32,6 @@ export interface TextToAudioParameters { /** * Parametrization of the text generation process - * - * Ad-hoc parametrization of the text generation process */ export interface GenerationParameters { /** diff --git a/packages/tasks/src/tasks/text-to-audio/spec/input.json b/packages/tasks/src/tasks/text-to-audio/spec/input.json index 99390c8a9..a836c53d5 100644 --- a/packages/tasks/src/tasks/text-to-audio/spec/input.json +++ b/packages/tasks/src/tasks/text-to-audio/spec/input.json @@ -10,14 +10,13 @@ "type": "string" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Text To Audio", "$ref": "#/$defs/TextToAudioParameters" } }, "$defs": { "TextToAudioParameters": { "title": "TextToAudioParameters", - "description": "Additional inference parameters for Text To Audio", "type": "object", "properties": { "generation_parameters": { diff --git a/packages/tasks/src/tasks/text-to-image/data.ts b/packages/tasks/src/tasks/text-to-image/data.ts index 82d5473e7..ebf1347d3 100644 --- a/packages/tasks/src/tasks/text-to-image/data.ts +++ b/packages/tasks/src/tasks/text-to-image/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/text-to-image/inference.ts b/packages/tasks/src/tasks/text-to-image/inference.ts index 8c30d3e9e..ae8a3b6a3 100644 --- a/packages/tasks/src/tasks/text-to-image/inference.ts +++ b/packages/tasks/src/tasks/text-to-image/inference.ts @@ -13,15 +13,13 @@ export interface TextToImageInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Text To Image */ parameters?: TextToImageParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Text To Image */ export interface TextToImageParameters { diff --git a/packages/tasks/src/tasks/text-to-image/spec/input.json b/packages/tasks/src/tasks/text-to-image/spec/input.json index 569f3c33a..bc22f88c2 100644 --- a/packages/tasks/src/tasks/text-to-image/spec/input.json +++ b/packages/tasks/src/tasks/text-to-image/spec/input.json @@ -10,14 +10,13 @@ "type": "string" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Text To Image", "$ref": "#/$defs/TextToImageParameters" } }, "$defs": { "TextToImageParameters": { "title": "TextToImageParameters", - "description": "Additional inference parameters for Text To Image", "type": "object", "properties": { "guidance_scale": { diff --git a/packages/tasks/src/tasks/text-to-speech/data.ts b/packages/tasks/src/tasks/text-to-speech/data.ts index 43a69ce4c..70ac91898 100644 --- a/packages/tasks/src/tasks/text-to-speech/data.ts +++ b/packages/tasks/src/tasks/text-to-speech/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { canonicalId: "text-to-audio", @@ -11,6 +11,10 @@ const taskData: TaskDataCustom = { description: "Multi-speaker English dataset.", id: "mythicinfinity/libritts_r", }, + { + description: "Mulit-lingual dataset.", + id: "facebook/multilingual_librispeech", + }, ], demo: { inputs: [ @@ -35,20 +39,24 @@ const taskData: TaskDataCustom = { ], models: [ { - description: "A powerful TTS model.", + description: "A prompt based, powerful TTS model.", id: "parler-tts/parler-tts-large-v1", }, + { + description: "A powerful TTS model that supports English and Chinese.", + id: "SWivid/F5-TTS", + }, { description: "A massively multi-lingual TTS model.", id: "coqui/XTTS-v2", }, { - description: "Robust TTS model.", - id: "metavoiceio/metavoice-1B-v0.1", + description: "A powerful TTS model.", + id: "amphion/MaskGCT", }, { - description: "A prompt based, powerful TTS model.", - id: "parler-tts/parler_tts_mini_v0.1", + description: "A Llama based TTS model.", + id: "OuteAI/OuteTTS-0.1-350M", }, ], spaces: [ @@ -57,7 +65,8 @@ const taskData: TaskDataCustom = { id: "suno/bark", }, { - description: "An application on XTTS, a voice generation model that lets you clone voices into different languages.", + description: + "An application on XTTS, a voice generation model that lets you clone voices into different languages.", id: "coqui/xtts", }, { @@ -65,8 +74,8 @@ const taskData: TaskDataCustom = { id: "mrfakename/E2-F5-TTS", }, { - description: "An application that synthesizes speech for diverse speaker prompts.", - id: "parler-tts/parler_tts_mini", + description: "An application that synthesizes emotional speech for diverse speaker prompts.", + id: "parler-tts/parler-tts-expresso", }, ], summary: diff --git a/packages/tasks/src/tasks/text-to-speech/inference.ts b/packages/tasks/src/tasks/text-to-speech/inference.ts index 765f7526a..230aad902 100644 --- a/packages/tasks/src/tasks/text-to-speech/inference.ts +++ b/packages/tasks/src/tasks/text-to-speech/inference.ts @@ -13,15 +13,13 @@ export interface TextToSpeechInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Text To Speech */ parameters?: TextToSpeechParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Text To Speech */ export interface TextToSpeechParameters { @@ -34,8 +32,6 @@ export interface TextToSpeechParameters { /** * Parametrization of the text generation process - * - * Ad-hoc parametrization of the text generation process */ export interface GenerationParameters { /** diff --git a/packages/tasks/src/tasks/text-to-speech/spec/input.json b/packages/tasks/src/tasks/text-to-speech/spec/input.json index 825d1c30d..a643f2d7c 100644 --- a/packages/tasks/src/tasks/text-to-speech/spec/input.json +++ b/packages/tasks/src/tasks/text-to-speech/spec/input.json @@ -10,14 +10,13 @@ "type": "string" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Text To Speech", "$ref": "#/$defs/TextToSpeechParameters" } }, "$defs": { "TextToSpeechParameters": { "title": "TextToSpeechParameters", - "description": "Additional inference parameters for Text To Speech", "type": "object", "properties": { "generation_parameters": { diff --git a/packages/tasks/src/tasks/text-to-video/data.ts b/packages/tasks/src/tasks/text-to-video/data.ts index 27f4925ba..ad9867de2 100644 --- a/packages/tasks/src/tasks/text-to-video/data.ts +++ b/packages/tasks/src/tasks/text-to-video/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/text2text-generation/inference.ts b/packages/tasks/src/tasks/text2text-generation/inference.ts index 3fb690b70..6bd9dab81 100644 --- a/packages/tasks/src/tasks/text2text-generation/inference.ts +++ b/packages/tasks/src/tasks/text2text-generation/inference.ts @@ -13,15 +13,13 @@ export interface Text2TextGenerationInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Text2text Generation */ parameters?: Text2TextGenerationParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Text2text Generation */ export interface Text2TextGenerationParameters { diff --git a/packages/tasks/src/tasks/text2text-generation/spec/input.json b/packages/tasks/src/tasks/text2text-generation/spec/input.json index 0310d7478..1b0456738 100644 --- a/packages/tasks/src/tasks/text2text-generation/spec/input.json +++ b/packages/tasks/src/tasks/text2text-generation/spec/input.json @@ -10,14 +10,13 @@ "type": "string" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Text2text Generation", "$ref": "#/$defs/Text2textGenerationParameters" } }, "$defs": { "Text2textGenerationParameters": { "title": "Text2textGenerationParameters", - "description": "Additional inference parameters for Text2text Generation", "type": "object", "properties": { "clean_up_tokenization_spaces": { diff --git a/packages/tasks/src/tasks/token-classification/data.ts b/packages/tasks/src/tasks/token-classification/data.ts index 7a1d1abed..2ecce1bcc 100644 --- a/packages/tasks/src/tasks/token-classification/data.ts +++ b/packages/tasks/src/tasks/token-classification/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/token-classification/inference.ts b/packages/tasks/src/tasks/token-classification/inference.ts index b218e08de..09f899c6b 100644 --- a/packages/tasks/src/tasks/token-classification/inference.ts +++ b/packages/tasks/src/tasks/token-classification/inference.ts @@ -12,14 +12,12 @@ export interface TokenClassificationInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Token Classification */ parameters?: TokenClassificationParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Token Classification */ export interface TokenClassificationParameters { diff --git a/packages/tasks/src/tasks/token-classification/spec/input.json b/packages/tasks/src/tasks/token-classification/spec/input.json index 30d6153d2..1176f3dd9 100644 --- a/packages/tasks/src/tasks/token-classification/spec/input.json +++ b/packages/tasks/src/tasks/token-classification/spec/input.json @@ -10,14 +10,13 @@ "type": "string" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Token Classification", "$ref": "#/$defs/TokenClassificationParameters" } }, "$defs": { "TokenClassificationParameters": { "title": "TokenClassificationParameters", - "description": "Additional inference parameters for Token Classification", "type": "object", "properties": { "ignore_labels": { diff --git a/packages/tasks/src/tasks/translation/data.ts b/packages/tasks/src/tasks/translation/data.ts index 9707734d9..535f9a84c 100644 --- a/packages/tasks/src/tasks/translation/data.ts +++ b/packages/tasks/src/tasks/translation/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { canonicalId: "text2text-generation", diff --git a/packages/tasks/src/tasks/translation/inference.ts b/packages/tasks/src/tasks/translation/inference.ts index a78c7e940..1e517e4c7 100644 --- a/packages/tasks/src/tasks/translation/inference.ts +++ b/packages/tasks/src/tasks/translation/inference.ts @@ -13,15 +13,13 @@ export interface TranslationInput { */ inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Translation */ parameters?: TranslationParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Translation */ export interface TranslationParameters { diff --git a/packages/tasks/src/tasks/translation/spec/input.json b/packages/tasks/src/tasks/translation/spec/input.json index 0c2d196cf..8f0a6fc6f 100644 --- a/packages/tasks/src/tasks/translation/spec/input.json +++ b/packages/tasks/src/tasks/translation/spec/input.json @@ -10,14 +10,13 @@ "type": "string" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Translation", "$ref": "#/$defs/TranslationParameters" } }, "$defs": { "TranslationParameters": { "title": "TranslationParameters", - "description": "Additional inference parameters for Translation", "type": "object", "properties": { "src_lang": { diff --git a/packages/tasks/src/tasks/unconditional-image-generation/data.ts b/packages/tasks/src/tasks/unconditional-image-generation/data.ts index 8cbf8a016..fcd66648e 100644 --- a/packages/tasks/src/tasks/unconditional-image-generation/data.ts +++ b/packages/tasks/src/tasks/unconditional-image-generation/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/video-classification/data.ts b/packages/tasks/src/tasks/video-classification/data.ts index 47d2c2d75..1e5abbce9 100644 --- a/packages/tasks/src/tasks/video-classification/data.ts +++ b/packages/tasks/src/tasks/video-classification/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/video-classification/inference.ts b/packages/tasks/src/tasks/video-classification/inference.ts index 6615b8ddc..5c937d9c1 100644 --- a/packages/tasks/src/tasks/video-classification/inference.ts +++ b/packages/tasks/src/tasks/video-classification/inference.ts @@ -12,14 +12,12 @@ export interface VideoClassificationInput { */ inputs: unknown; /** - * Additional inference parameters + * Additional inference parameters for Video Classification */ parameters?: VideoClassificationParameters; [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Video Classification */ export interface VideoClassificationParameters { @@ -27,6 +25,9 @@ export interface VideoClassificationParameters { * The sampling rate used to select frames from the video. */ frame_sampling_rate?: number; + /** + * The function to apply to the model outputs in order to retrieve the scores. + */ function_to_apply?: ClassificationOutputTransform; /** * The number of sampled frames to consider for classification. diff --git a/packages/tasks/src/tasks/video-classification/spec/input.json b/packages/tasks/src/tasks/video-classification/spec/input.json index 1fb58e278..277950e59 100644 --- a/packages/tasks/src/tasks/video-classification/spec/input.json +++ b/packages/tasks/src/tasks/video-classification/spec/input.json @@ -9,19 +9,19 @@ "description": "The input video data" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Video Classification", "$ref": "#/$defs/VideoClassificationParameters" } }, "$defs": { "VideoClassificationParameters": { "title": "VideoClassificationParameters", - "description": "Additional inference parameters for Video Classification", "type": "object", "properties": { "function_to_apply": { "title": "TextClassificationOutputTransform", - "$ref": "/inference/schemas/common-definitions.json#/definitions/ClassificationOutputTransform" + "$ref": "/inference/schemas/common-definitions.json#/definitions/ClassificationOutputTransform", + "description": "The function to apply to the model outputs in order to retrieve the scores." }, "num_frames": { "type": "integer", diff --git a/packages/tasks/src/tasks/video-text-to-text/data.ts b/packages/tasks/src/tasks/video-text-to-text/data.ts index eed4add06..bca190bc5 100644 --- a/packages/tasks/src/tasks/video-text-to-text/data.ts +++ b/packages/tasks/src/tasks/video-text-to-text/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/visual-question-answering/data.ts b/packages/tasks/src/tasks/visual-question-answering/data.ts index f31334330..51de32a80 100644 --- a/packages/tasks/src/tasks/visual-question-answering/data.ts +++ b/packages/tasks/src/tasks/visual-question-answering/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/visual-question-answering/inference.ts b/packages/tasks/src/tasks/visual-question-answering/inference.ts index fbc53f11b..8b774988a 100644 --- a/packages/tasks/src/tasks/visual-question-answering/inference.ts +++ b/packages/tasks/src/tasks/visual-question-answering/inference.ts @@ -12,7 +12,7 @@ export interface VisualQuestionAnsweringInput { */ inputs: VisualQuestionAnsweringInputData; /** - * Additional inference parameters + * Additional inference parameters for Visual Question Answering */ parameters?: VisualQuestionAnsweringParameters; [property: string]: unknown; @@ -32,8 +32,6 @@ export interface VisualQuestionAnsweringInputData { [property: string]: unknown; } /** - * Additional inference parameters - * * Additional inference parameters for Visual Question Answering */ export interface VisualQuestionAnsweringParameters { diff --git a/packages/tasks/src/tasks/visual-question-answering/spec/input.json b/packages/tasks/src/tasks/visual-question-answering/spec/input.json index 9f9dab121..0867bfcf2 100644 --- a/packages/tasks/src/tasks/visual-question-answering/spec/input.json +++ b/packages/tasks/src/tasks/visual-question-answering/spec/input.json @@ -20,14 +20,13 @@ "required": ["question", "image"] }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Visual Question Answering", "$ref": "#/$defs/VisualQuestionAnsweringParameters" } }, "$defs": { "VisualQuestionAnsweringParameters": { "title": "VisualQuestionAnsweringParameters", - "description": "Additional inference parameters for Visual Question Answering", "type": "object", "properties": { "top_k": { diff --git a/packages/tasks/src/tasks/zero-shot-classification/data.ts b/packages/tasks/src/tasks/zero-shot-classification/data.ts index 1ecc51c95..516efa4d1 100644 --- a/packages/tasks/src/tasks/zero-shot-classification/data.ts +++ b/packages/tasks/src/tasks/zero-shot-classification/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/zero-shot-classification/inference.ts b/packages/tasks/src/tasks/zero-shot-classification/inference.ts index 20e0d369a..aaae36f62 100644 --- a/packages/tasks/src/tasks/zero-shot-classification/inference.ts +++ b/packages/tasks/src/tasks/zero-shot-classification/inference.ts @@ -8,38 +8,26 @@ */ export interface ZeroShotClassificationInput { /** - * The input text data, with candidate labels + * The text to classify */ - inputs: ZeroShotClassificationInputData; + inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Zero Shot Classification */ - parameters?: ZeroShotClassificationParameters; + parameters: ZeroShotClassificationParameters; [property: string]: unknown; } /** - * The input text data, with candidate labels + * Additional inference parameters for Zero Shot Classification */ -export interface ZeroShotClassificationInputData { +export interface ZeroShotClassificationParameters { /** * The set of possible class labels to classify the text into. */ - candidateLabels: string[]; - /** - * The text to classify - */ - text: string; - [property: string]: unknown; -} -/** - * Additional inference parameters - * - * Additional inference parameters for Zero Shot Classification - */ -export interface ZeroShotClassificationParameters { + candidate_labels: string[]; /** - * The sentence used in conjunction with candidateLabels to attempt the text classification - * by replacing the placeholder with the candidate labels. + * The sentence used in conjunction with `candidate_labels` to attempt the text + * classification by replacing the placeholder with the candidate labels. */ hypothesis_template?: string; /** diff --git a/packages/tasks/src/tasks/zero-shot-classification/spec/input.json b/packages/tasks/src/tasks/zero-shot-classification/spec/input.json index c955f2769..c919eac7e 100644 --- a/packages/tasks/src/tasks/zero-shot-classification/spec/input.json +++ b/packages/tasks/src/tasks/zero-shot-classification/spec/input.json @@ -6,45 +6,37 @@ "type": "object", "properties": { "inputs": { - "description": "The input text data, with candidate labels", - "type": "object", - "title": "ZeroShotClassificationInputData", - "properties": { - "text": { - "type": "string", - "description": "The text to classify" - }, - "candidateLabels": { - "type": "array", - "description": "The set of possible class labels to classify the text into.", - "items": { - "type": "string" - } - } - }, - "required": ["text", "candidateLabels"] + "description": "The text to classify", + "type": "string" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Zero Shot Classification", "$ref": "#/$defs/ZeroShotClassificationParameters" } }, "$defs": { "ZeroShotClassificationParameters": { "title": "ZeroShotClassificationParameters", - "description": "Additional inference parameters for Zero Shot Classification", "type": "object", "properties": { + "candidate_labels": { + "type": "array", + "description": "The set of possible class labels to classify the text into.", + "items": { + "type": "string" + } + }, "hypothesis_template": { "type": "string", - "description": "The sentence used in conjunction with candidateLabels to attempt the text classification by replacing the placeholder with the candidate labels." + "description": "The sentence used in conjunction with `candidate_labels` to attempt the text classification by replacing the placeholder with the candidate labels." }, "multi_label": { "type": "boolean", "description": "Whether multiple candidate labels can be true. If false, the scores are normalized such that the sum of the label likelihoods for each sequence is 1. If true, the labels are considered independent and probabilities are normalized for each candidate." } - } + }, + "required": ["candidate_labels"] } }, - "required": ["inputs"] + "required": ["inputs", "parameters"] } diff --git a/packages/tasks/src/tasks/zero-shot-image-classification/data.ts b/packages/tasks/src/tasks/zero-shot-image-classification/data.ts index 5be19aedf..776a6d194 100644 --- a/packages/tasks/src/tasks/zero-shot-image-classification/data.ts +++ b/packages/tasks/src/tasks/zero-shot-image-classification/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [ diff --git a/packages/tasks/src/tasks/zero-shot-image-classification/inference.ts b/packages/tasks/src/tasks/zero-shot-image-classification/inference.ts index 44ce76173..594a4b562 100644 --- a/packages/tasks/src/tasks/zero-shot-image-classification/inference.ts +++ b/packages/tasks/src/tasks/zero-shot-image-classification/inference.ts @@ -8,38 +8,26 @@ */ export interface ZeroShotImageClassificationInput { /** - * The input image data, with candidate labels + * The input image data to classify as a base64-encoded string. */ - inputs: ZeroShotImageClassificationInputData; + inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Zero Shot Image Classification */ - parameters?: ZeroShotImageClassificationParameters; + parameters: ZeroShotImageClassificationParameters; [property: string]: unknown; } /** - * The input image data, with candidate labels + * Additional inference parameters for Zero Shot Image Classification */ -export interface ZeroShotImageClassificationInputData { +export interface ZeroShotImageClassificationParameters { /** * The candidate labels for this image */ - candidateLabels: string[]; - /** - * The image data to classify - */ - image: unknown; - [property: string]: unknown; -} -/** - * Additional inference parameters - * - * Additional inference parameters for Zero Shot Image Classification - */ -export interface ZeroShotImageClassificationParameters { + candidate_labels: string[]; /** - * The sentence used in conjunction with candidateLabels to attempt the text classification - * by replacing the placeholder with the candidate labels. + * The sentence used in conjunction with `candidate_labels` to attempt the image + * classification by replacing the placeholder with the candidate labels. */ hypothesis_template?: string; [property: string]: unknown; diff --git a/packages/tasks/src/tasks/zero-shot-image-classification/spec/input.json b/packages/tasks/src/tasks/zero-shot-image-classification/spec/input.json index dfdababc7..5795dd287 100644 --- a/packages/tasks/src/tasks/zero-shot-image-classification/spec/input.json +++ b/packages/tasks/src/tasks/zero-shot-image-classification/spec/input.json @@ -6,40 +6,33 @@ "type": "object", "properties": { "inputs": { - "description": "The input image data, with candidate labels", - "type": "object", - "title": "ZeroShotImageClassificationInputData", - "properties": { - "image": { - "description": "The image data to classify" - }, - "candidateLabels": { - "description": "The candidate labels for this image", - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["image", "candidateLabels"] + "type": "string", + "description": "The input image data to classify as a base64-encoded string." }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Zero Shot Image Classification", "$ref": "#/$defs/ZeroShotImageClassificationParameters" } }, "$defs": { "ZeroShotImageClassificationParameters": { "title": "ZeroShotImageClassificationParameters", - "description": "Additional inference parameters for Zero Shot Image Classification", "type": "object", "properties": { + "candidate_labels": { + "description": "The candidate labels for this image", + "type": "array", + "items": { + "type": "string" + } + }, "hypothesis_template": { "type": "string", - "description": "The sentence used in conjunction with candidateLabels to attempt the text classification by replacing the placeholder with the candidate labels." + "description": "The sentence used in conjunction with `candidate_labels` to attempt the image classification by replacing the placeholder with the candidate labels." } - } + }, + "required": ["candidate_labels"] } }, - "required": ["inputs"] + "required": ["inputs", "parameters"] } diff --git a/packages/tasks/src/tasks/zero-shot-object-detection/data.ts b/packages/tasks/src/tasks/zero-shot-object-detection/data.ts index 9e36cad46..6446704d4 100644 --- a/packages/tasks/src/tasks/zero-shot-object-detection/data.ts +++ b/packages/tasks/src/tasks/zero-shot-object-detection/data.ts @@ -1,4 +1,4 @@ -import type { TaskDataCustom } from ".."; +import type { TaskDataCustom } from "../index.js"; const taskData: TaskDataCustom = { datasets: [], diff --git a/packages/tasks/src/tasks/zero-shot-object-detection/inference.ts b/packages/tasks/src/tasks/zero-shot-object-detection/inference.ts index 87447ca0a..860609547 100644 --- a/packages/tasks/src/tasks/zero-shot-object-detection/inference.ts +++ b/packages/tasks/src/tasks/zero-shot-object-detection/inference.ts @@ -8,29 +8,23 @@ */ export interface ZeroShotObjectDetectionInput { /** - * The input image data, with candidate labels + * The input image data as a base64-encoded string. */ - inputs: ZeroShotObjectDetectionInputData; + inputs: string; /** - * Additional inference parameters + * Additional inference parameters for Zero Shot Object Detection */ - parameters?: { - [key: string]: unknown; - }; + parameters: ZeroShotObjectDetectionParameters; [property: string]: unknown; } /** - * The input image data, with candidate labels + * Additional inference parameters for Zero Shot Object Detection */ -export interface ZeroShotObjectDetectionInputData { +export interface ZeroShotObjectDetectionParameters { /** * The candidate labels for this image */ - candidateLabels: string[]; - /** - * The image data to generate bounding boxes from - */ - image: unknown; + candidate_labels: string[]; [property: string]: unknown; } /** diff --git a/packages/tasks/src/tasks/zero-shot-object-detection/spec/input.json b/packages/tasks/src/tasks/zero-shot-object-detection/spec/input.json index 7c9aa15ac..d3532be05 100644 --- a/packages/tasks/src/tasks/zero-shot-object-detection/spec/input.json +++ b/packages/tasks/src/tasks/zero-shot-object-detection/spec/input.json @@ -6,35 +6,29 @@ "type": "object", "properties": { "inputs": { - "description": "The input image data, with candidate labels", - "type": "object", - "title": "ZeroShotObjectDetectionInputData", - "properties": { - "image": { - "description": "The image data to generate bounding boxes from" - }, - "candidateLabels": { - "description": "The candidate labels for this image", - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["image", "candidateLabels"] + "description": "The input image data as a base64-encoded string.", + "type": "string" }, "parameters": { - "description": "Additional inference parameters", + "description": "Additional inference parameters for Zero Shot Object Detection", "$ref": "#/$defs/ZeroShotObjectDetectionParameters" } }, "$defs": { "ZeroShotObjectDetectionParameters": { "title": "ZeroShotObjectDetectionParameters", - "description": "Additional inference parameters for Zero Shot Object Detection", "type": "object", - "properties": {} + "properties": { + "candidate_labels": { + "description": "The candidate labels for this image", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["candidate_labels"] } }, - "required": ["inputs"] + "required": ["inputs", "parameters"] } diff --git a/packages/tasks/src/widget-example.ts b/packages/tasks/src/widget-example.ts index 3c47530f4..780953bca 100644 --- a/packages/tasks/src/widget-example.ts +++ b/packages/tasks/src/widget-example.ts @@ -2,7 +2,7 @@ * See default-widget-inputs.ts for the default widget inputs, this files only contains the types */ -import type { ChatCompletionInputMessage } from "./tasks"; +import type { ChatCompletionInputMessage } from "./tasks/index.js"; type TableData = Record; diff --git a/packages/tasks/tsconfig.json b/packages/tasks/tsconfig.json index 6a2bad758..3e3c571e5 100644 --- a/packages/tasks/tsconfig.json +++ b/packages/tasks/tsconfig.json @@ -2,9 +2,9 @@ "compilerOptions": { "allowSyntheticDefaultImports": true, "lib": ["ES2022", "DOM"], - "module": "ESNext", + "module": "NodeNext", "target": "ESNext", - "moduleResolution": "node", + "moduleResolution": "nodenext", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitAny": true, @@ -15,6 +15,6 @@ "declaration": true, "declarationMap": true }, - "include": ["src", "scripts"], + "include": ["src"], "exclude": ["dist"] } diff --git a/packages/widgets/src/lib/components/PipelineIcon/PipelineIcon.svelte b/packages/widgets/src/lib/components/PipelineIcon/PipelineIcon.svelte index 1429e1cb4..0d85bf76a 100644 --- a/packages/widgets/src/lib/components/PipelineIcon/PipelineIcon.svelte +++ b/packages/widgets/src/lib/components/PipelineIcon/PipelineIcon.svelte @@ -73,6 +73,7 @@ "automatic-speech-recognition": IconAutomaticSpeechRecognition, "audio-to-audio": IconAudioToAudio, "audio-classification": IconAudioClassification, + "audio-text-to-text": IconAudioToAudio, "voice-activity-detection": IconVoiceActivityDetection, "depth-estimation": IconDepthEstimation, "image-classification": IconImageClassification, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b93520c40..53ea212e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false devDependencies: + '@types/node': + specifier: ^18.16.1 + version: 18.19.61 '@typescript-eslint/eslint-plugin': specifier: ^7.2.0 version: 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2) @@ -38,6 +41,9 @@ devDependencies: semver: specifier: ^7.5.0 version: 7.5.0 + tshy: + specifier: ^3.0.2 + version: 3.0.2 tsup: specifier: ^6.7.0 version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) @@ -49,7 +55,7 @@ devDependencies: version: 5.4.2 vite: specifier: ^5.0.2 - version: 5.0.2(@types/node@20.10.0) + version: 5.0.2(@types/node@18.19.61) vitest: specifier: ^0.34.6 version: 0.34.6(@vitest/browser@0.34.6)(webdriverio@8.6.7) @@ -951,6 +957,18 @@ packages: resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} dev: true + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1181,12 +1199,8 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true - /@types/node@18.14.6: - resolution: {integrity: sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==} - dev: true - - /@types/node@20.10.0: - resolution: {integrity: sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==} + /@types/node@18.19.61: + resolution: {integrity: sha512-z8fH66NcVkDzBItOao+Nyh0fiy7CYdxIyxnNCcZ60aY0I+EA/y4TSi/S/W9i8DIQvwVo7a0pgzAxmDeNnqrpkw==} dependencies: undici-types: 5.26.5 dev: true @@ -1206,14 +1220,14 @@ packages: /@types/ws@8.5.4: resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} dependencies: - '@types/node': 18.14.6 + '@types/node': 18.19.61 dev: true /@types/yauzl@2.10.3: resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} requiresBuild: true dependencies: - '@types/node': 18.14.6 + '@types/node': 18.19.61 dev: true optional: true @@ -1438,14 +1452,14 @@ packages: resolution: {integrity: sha512-vyJzqHJ5yOmfVyk5WWo6pRsJ2xhgWl3DVIBdDNR0wKrtFcm/g1jnB+pNf6Eb7NhCDh3oGul25bmhAwWDoxcFYA==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.14.6 + '@types/node': 18.19.61 dev: true /@wdio/types@8.4.0: resolution: {integrity: sha512-1eA0D0jS8Ttg67zB2gsZJFUcHcRz4VRjLTjxdLKh70+ZfB1+YZr9tScLgQjc+qsjsK1wKSzOz03uZiidwNnN9g==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.14.6 + '@types/node': 18.19.61 dev: true /@wdio/utils@8.6.6: @@ -1514,6 +1528,11 @@ packages: engines: {node: '>=8'} dev: true + /ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + dev: true + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -1533,6 +1552,11 @@ packages: engines: {node: '>=10'} dev: true + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + /any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} dev: true @@ -1786,6 +1810,11 @@ packages: engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} dev: true + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: true + /check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} dependencies: @@ -1807,6 +1836,21 @@ packages: fsevents: 2.3.3 dev: true + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} dev: true @@ -1816,7 +1860,7 @@ packages: engines: {node: '>=12.13.0'} hasBin: true dependencies: - '@types/node': 18.14.6 + '@types/node': 18.19.61 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.3.0 @@ -2093,7 +2137,7 @@ packages: resolution: {integrity: sha512-ea6a7XqflRSVe/eJ28eaJJtjrpoqP3OeZ5fyI7SIGGYvyuw9Ha7ZTL9aMYB9TBDw15iNXjIa1a/wuOjQVd80jA==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.14.6 + '@types/node': 18.19.61 '@wdio/config': 8.6.6 '@wdio/logger': 8.6.6 '@wdio/protocols': 8.6.6 @@ -2134,6 +2178,10 @@ packages: esutils: 2.0.3 dev: true + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + /edge-paths@3.0.5: resolution: {integrity: sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==} engines: {node: '>=14.0.0'} @@ -2142,6 +2190,14 @@ packages: which: 2.0.2 dev: true + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + /end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: @@ -2685,6 +2741,14 @@ packages: is-callable: 1.2.7 dev: true + /foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + /form-data-encoder@2.1.4: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} @@ -2792,6 +2856,19 @@ packages: is-glob: 4.0.3 dev: true + /glob@11.0.0: + resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} + engines: {node: 20 || >=22} + hasBin: true + dependencies: + foreground-child: 3.3.0 + jackspeak: 4.0.2 + minimatch: 10.0.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + dev: true + /glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} dependencies: @@ -3135,6 +3212,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -3277,6 +3359,13 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /jackspeak@4.0.2: + resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==} + engines: {node: 20 || >=22} + dependencies: + '@isaacs/cliui': 8.0.2 + dev: true + /joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -3459,6 +3548,11 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true + /lru-cache@11.0.2: + resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} + engines: {node: 20 || >=22} + dev: true + /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -3523,6 +3617,13 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true + /minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -3555,6 +3656,11 @@ packages: engines: {node: '>=8'} dev: true + /minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + /mitt@3.0.0: resolution: {integrity: sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==} dev: true @@ -3563,6 +3669,12 @@ packages: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} dev: true + /mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + dev: true + /mlly@1.4.2: resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} dependencies: @@ -3795,6 +3907,10 @@ packages: p-limit: 4.0.0 dev: true + /package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + dev: true + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3857,6 +3973,14 @@ packages: minipass: 4.2.5 dev: true + /path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + dependencies: + lru-cache: 11.0.2 + minipass: 7.1.2 + dev: true + /path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} engines: {node: '>=4'} @@ -3922,6 +4046,11 @@ packages: pathe: 1.1.1 dev: true + /polite-json@5.0.0: + resolution: {integrity: sha512-OLS/0XeUAcE8a2fdwemNja+udKgXNnY6yKVIXqAD2zVRx1KvY6Ato/rZ2vdzbxqYwPW0u6SCNC/bAMPNzpzxbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + /possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -4159,6 +4288,14 @@ packages: engines: {node: '>=8'} dev: true + /resolve-import@2.0.0: + resolution: {integrity: sha512-jpKjLibLuc8D1XEV2+7zb0aqN7I8d12u89g/v6IsgCzdVlccMQJq4TKkPw5fbhHdxhm7nbVtN+KvOTnjFf+nEA==} + engines: {node: 20 || >=22} + dependencies: + glob: 11.0.0 + walk-up-path: 4.0.0 + dev: true + /resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: true @@ -4209,6 +4346,15 @@ packages: glob: 9.3.1 dev: true + /rimraf@6.0.1: + resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} + engines: {node: 20 || >=22} + hasBin: true + dependencies: + glob: 11.0.0 + package-json-from-dist: 1.0.1 + dev: true + /rollup@3.18.0: resolution: {integrity: sha512-J8C6VfEBjkvYPESMQYxKHxNOh4A5a3FlP+0BETGo34HEcE4eTlgCrO2+eWzlu2a/sHs2QUkZco+wscH7jhhgWg==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -4364,6 +4510,11 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + /sirv@2.0.3: resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} engines: {node: '>= 10'} @@ -4427,6 +4578,24 @@ packages: internal-slot: 1.0.5 dev: true + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + /string.prototype.padend@3.1.6: resolution: {integrity: sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==} engines: {node: '>= 0.4'} @@ -4483,6 +4652,13 @@ packages: ansi-regex: 5.0.1 dev: true + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.1.0 + dev: true + /strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -4571,6 +4747,18 @@ packages: periscopic: 3.1.0 dev: true + /sync-content@2.0.1: + resolution: {integrity: sha512-NI1mo514yFhr8pV/5Etvgh+pSBUIpoAKoiBIUwALVlQQNAwb40bTw8hhPFaip/dvv0GhpHVOq0vq8iY02ppLTg==} + engines: {node: 20 || >=22} + hasBin: true + dependencies: + glob: 11.0.0 + mkdirp: 3.0.1 + path-scurry: 2.0.0 + rimraf: 6.0.1 + tshy: 3.0.2 + dev: true + /tar-fs@2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} dependencies: @@ -4666,6 +4854,24 @@ packages: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true + /tshy@3.0.2: + resolution: {integrity: sha512-8GkWnAfmNXxl8iDTZ1o2H4jdaj9H7HeDKkr5qd0ZhQBCNA41D3xqTyg2Ycs51VCfmjJ5e+0v9AUmD6ylAI9Bgw==} + engines: {node: 20 || >=22} + hasBin: true + dependencies: + chalk: 5.3.0 + chokidar: 3.6.0 + foreground-child: 3.3.0 + minimatch: 10.0.1 + mkdirp: 3.0.1 + polite-json: 5.0.0 + resolve-import: 2.0.0 + rimraf: 6.0.1 + sync-content: 2.0.1 + typescript: 5.6.3 + walk-up-path: 4.0.0 + dev: true + /tsup@6.7.0(postcss@8.4.31)(typescript@5.4.2): resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==} engines: {node: '>=14.18'} @@ -4786,6 +4992,12 @@ packages: hasBin: true dev: true + /typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + /ua-parser-js@1.0.34: resolution: {integrity: sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==} dev: true @@ -4836,7 +5048,7 @@ packages: spdx-expression-parse: 3.0.1 dev: true - /vite-node@0.34.6(@types/node@20.10.0): + /vite-node@0.34.6(@types/node@18.19.61): resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} engines: {node: '>=v14.18.0'} hasBin: true @@ -4846,7 +5058,7 @@ packages: mlly: 1.4.2 pathe: 1.1.1 picocolors: 1.0.0 - vite: 5.0.2(@types/node@20.10.0) + vite: 5.0.2(@types/node@18.19.61) transitivePeerDependencies: - '@types/node' - less @@ -4858,7 +5070,7 @@ packages: - terser dev: true - /vite@5.0.2(@types/node@20.10.0): + /vite@5.0.2(@types/node@18.19.61): resolution: {integrity: sha512-6CCq1CAJCNM1ya2ZZA7+jS2KgnhbzvxakmlIjN24cF/PXhRMzpM/z8QgsVJA/Dm5fWUWnVEsmtBoMhmerPxT0g==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -4886,7 +5098,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.10.0 + '@types/node': 18.19.61 esbuild: 0.19.8 postcss: 8.4.31 rollup: 4.6.0 @@ -4927,7 +5139,7 @@ packages: dependencies: '@types/chai': 4.3.11 '@types/chai-subset': 1.3.5 - '@types/node': 20.10.0 + '@types/node': 18.19.61 '@vitest/browser': 0.34.6(esbuild@0.18.20)(vitest@0.34.6) '@vitest/expect': 0.34.6 '@vitest/runner': 0.34.6 @@ -4947,8 +5159,8 @@ packages: strip-literal: 1.3.0 tinybench: 2.5.1 tinypool: 0.7.0 - vite: 5.0.2(@types/node@20.10.0) - vite-node: 0.34.6(@types/node@20.10.0) + vite: 5.0.2(@types/node@18.19.61) + vite-node: 0.34.6(@types/node@18.19.61) webdriverio: 8.6.7(typescript@5.4.2) why-is-node-running: 2.2.2 transitivePeerDependencies: @@ -4961,11 +5173,16 @@ packages: - terser dev: true + /walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + dev: true + /webdriver@8.6.6: resolution: {integrity: sha512-cly5yf5BnHOrpF+wkT7ztlt++wjws63sIyfPGPVt9MQD+yutt3zW1dB1bXgAGVVvVQUAeIkjcrIc1t8+1crFgQ==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.14.6 + '@types/node': 18.19.61 '@types/ws': 8.5.4 '@wdio/config': 8.6.6 '@wdio/logger': 8.6.6 @@ -4985,7 +5202,7 @@ packages: resolution: {integrity: sha512-uaBovfcJQ+g5olfveeku2GT6kGx8qZ74fVvxZVRM/6FYWK6eMT//MPVnmM9UducYHApcxXFULrA+UTtOY5EEag==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.14.6 + '@types/node': 18.19.61 '@wdio/config': 8.6.6 '@wdio/logger': 8.6.6 '@wdio/protocols': 8.6.6 @@ -5115,6 +5332,24 @@ packages: stackback: 0.0.2 dev: true + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 159379aaa..0d2e7bba3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,10 +1,13 @@ packages: + - "packages/blob" + - "packages/dduf" - "packages/hub" - "packages/inference" - "packages/doc-internal" - "packages/agents" - "packages/languages" - "packages/tasks" + - "packages/tasks-gen" - "packages/gguf" - "packages/jinja" - "packages/space-header" diff --git a/scripts/check-deps.ts b/scripts/check-deps.ts new file mode 100644 index 000000000..dd29a34b3 --- /dev/null +++ b/scripts/check-deps.ts @@ -0,0 +1,53 @@ +import { execSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { parseArgs } from "node:util"; + +const args = parseArgs({ + allowPositionals: true, +}); + +const dep = args.positionals[0]; + +if (!dep) { + console.error("Error: No dependency specified."); + process.exit(1); +} + +process.chdir(`./packages/${dep}`); + +const localPackageJson = readFileSync(`./package.json`, "utf-8"); +const localVersion = JSON.parse(localPackageJson).version as string; +const remoteVersion = execSync(`npm view @huggingface/${dep} version`).toString().trim(); + +if (localVersion !== remoteVersion) { + console.error( + `Error: The local @huggingface/${dep} package version (${localVersion}) differs from the remote version (${remoteVersion}). Release halted.` + ); + process.exit(1); +} + +execSync(`npm pack`); +execSync(`mv huggingface-${dep}-${localVersion}.tgz ${dep}-local.tgz`); + +execSync(`npm pack @huggingface/${dep}@${remoteVersion}`); +execSync(`mv huggingface-${dep}-${remoteVersion}.tgz ${dep}-remote.tgz`); + +execSync(`rm -Rf local && mkdir local && tar -xf ${dep}-local.tgz -C local`); +execSync(`rm -Rf remote && mkdir remote && tar -xf ${dep}-remote.tgz -C remote`); + +// Remove package.json files because they're modified by npm +execSync(`rm local/package/package.json`); +execSync(`rm remote/package/package.json`); + +try { + execSync("diff --brief -r local remote").toString(); +} catch (e) { + console.error(e.output.filter(Boolean).join("\n")); + console.error(`Error: The local and remote @huggingface/${dep} packages are inconsistent. Release halted.`); + process.exit(1); +} + +console.log(`The local and remote @huggingface/${dep} packages are consistent.`); + +execSync(`rm -Rf local`); +execSync(`rm -Rf remote`);