From de29371d65b782fa355c0301604a581178829226 Mon Sep 17 00:00:00 2001 From: Neko Date: Thu, 26 Dec 2024 23:07:27 +0800 Subject: [PATCH] feat(hfsup): new integration, added back airi deploy to HF Spaces (#6) * fix: pwa issue, special `TARGET_HUGGINGFACE_SPACE` for hf spaces, half width issue --- .../workflows/deploy-huggingface-spaces.yml | 56 +-- cspell.config.yaml | 4 + packages/hfsup/README.md | 52 +++ packages/hfsup/package.json | 42 +++ packages/hfsup/src/utils/fs.ts | 25 ++ packages/hfsup/src/vite/index.ts | 2 + packages/hfsup/src/vite/lfs.ts | 74 ++++ packages/hfsup/src/vite/space-card/index.ts | 92 +++++ packages/hfsup/src/vite/space-card/types.ts | 338 ++++++++++++++++++ packages/hfsup/tsconfig.json | 15 + packages/moonshine-web/README.md | 14 - packages/moonshine-web/package.json | 1 + packages/moonshine-web/vite.config.ts | 16 + packages/stage/README.md | 20 +- packages/stage/package.json | 1 + .../stage/src/components/Widgets/Stage.vue | 4 +- packages/stage/src/main.ts | 17 +- packages/stage/src/pages/[...all].vue | 5 + packages/stage/src/pages/index.vue | 4 +- packages/stage/vite-env.d.ts | 10 + packages/stage/vite.config.ts | 79 ++-- packages/whisper-webgpu/README.md | 14 - packages/whisper-webgpu/package.json | 1 + packages/whisper-webgpu/vite.config.ts | 16 + pnpm-lock.yaml | 38 +- 25 files changed, 836 insertions(+), 104 deletions(-) create mode 100644 packages/hfsup/README.md create mode 100644 packages/hfsup/package.json create mode 100644 packages/hfsup/src/utils/fs.ts create mode 100644 packages/hfsup/src/vite/index.ts create mode 100644 packages/hfsup/src/vite/lfs.ts create mode 100644 packages/hfsup/src/vite/space-card/index.ts create mode 100644 packages/hfsup/src/vite/space-card/types.ts create mode 100644 packages/hfsup/tsconfig.json create mode 100644 packages/stage/src/pages/[...all].vue create mode 100644 packages/stage/vite-env.d.ts diff --git a/.github/workflows/deploy-huggingface-spaces.yml b/.github/workflows/deploy-huggingface-spaces.yml index 38b21cba2..f5b3bb836 100644 --- a/.github/workflows/deploy-huggingface-spaces.yml +++ b/.github/workflows/deploy-huggingface-spaces.yml @@ -26,11 +26,11 @@ jobs: HF_TOKEN: ${{ secrets.HF_TOKEN }} HF_USERNAME: ${{ secrets.HF_USERNAME }} - # - run: |- - # git clone https://$HF_USERNAME:$HF_TOKEN@huggingface.co/spaces/moeru-ai/airi --depth 1 packages/stage/dist - # env: - # HF_TOKEN: ${{ secrets.HF_TOKEN }} - # HF_USERNAME: ${{ secrets.HF_USERNAME }} + - run: |- + git clone https://$HF_USERNAME:$HF_TOKEN@huggingface.co/spaces/moeru-ai/airi --depth 1 packages/stage/dist + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + HF_USERNAME: ${{ secrets.HF_USERNAME }} - uses: pnpm/action-setup@v3 - uses: actions/setup-node@v4 @@ -40,11 +40,14 @@ jobs: - run: pnpm install - run: pnpm build + env: + TARGET_HUGGINGFACE_SPACE: 'true' + VITE_APP_TARGET_HUGGINGFACE_SPACE: 'true' - id: moonshine_web_diff working-directory: ./packages/moonshine-web/dist run: |- - mv ../README.md . + git lfs ls-files --all git add . if [[ -n $(git status --porcelain) ]]; then echo "changes=true" >> "$GITHUB_OUTPUT"; @@ -59,12 +62,13 @@ jobs: git config --local user.email "neko@ayaka.moe" git config --local user.name "Neko Ayaka" git commit -m "release: build ${{ github.sha }}" + git lfs push origin main --all git push -f - id: whisper_webgpu_diff working-directory: ./packages/whisper-webgpu/dist run: |- - mv ../README.md . + git lfs ls-files --all git add . if [[ -n $(git status --porcelain) ]]; then echo "changes=true" >> "$GITHUB_OUTPUT"; @@ -79,24 +83,26 @@ jobs: git config --local user.email "neko@ayaka.moe" git config --local user.name "Neko Ayaka" git commit -m "release: build ${{ github.sha }}" + git lfs push origin main --all git push -f - # - id: airi_diff - # working-directory: ./packages/stage/dist - # run: |- - # mv ../README.md . - # git add . - # if [[ -n $(git status --porcelain) ]]; then - # echo "changes=true" >> "$GITHUB_OUTPUT"; - # fi + - id: airi_diff + working-directory: ./packages/stage/dist + run: |- + git lfs ls-files --all + git add . + if [[ -n $(git status --porcelain) ]]; then + echo "changes=true" >> "$GITHUB_OUTPUT"; + fi - # - if: steps.airi_diff.outputs.changes == 'true' - # working-directory: ./packages/stage/dist - # env: - # HF_TOKEN: ${{ secrets.HF_TOKEN }} - # HF_USERNAME: ${{ secrets.HF_USERNAME }} - # run: |- - # git config --local user.email "neko@ayaka.moe" - # git config --local user.name "Neko Ayaka" - # git commit -m "release: build ${{ github.sha }}" - # git push -f + - if: steps.airi_diff.outputs.changes == 'true' + working-directory: ./packages/stage/dist + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + HF_USERNAME: ${{ secrets.HF_USERNAME }} + run: |- + git config --local user.email "neko@ayaka.moe" + git config --local user.name "Neko Ayaka" + git commit -m "release: build ${{ github.sha }}" + git lfs push origin main --all + git push -f diff --git a/cspell.config.yaml b/cspell.config.yaml index a648404ed..4e4420377 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -30,9 +30,12 @@ words: - elevenlabs - formkit - gltf + - hfspaceup + - hfsup - histoire - hiyori - huggingface + - huggingspace - iconify - intlify - kwaa @@ -71,6 +74,7 @@ words: - unocss - unplugin - valibot + - VITE - vrma - vueuse - webgpu diff --git a/packages/hfsup/README.md b/packages/hfsup/README.md new file mode 100644 index 000000000..e33cb5010 --- /dev/null +++ b/packages/hfsup/README.md @@ -0,0 +1,52 @@ +

πŸ§‘β€πŸš€ hfsup

+ +> `hfsup` stands for HuggingFace Space up, where the `up` is coming from `rollup`, `tsup`, you may think of "to make your work up and running". +> +> A collection of tools to help you deploy, bundle HuggingFace Spaces and related assets with ease. + +```shell +pnpm i hfsup -D +``` + +```ts +import { LFS, SpaceCard } from 'hfsup/vite' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [ + LFS(), + SpaceCard({ + title: 'Real-time Whisper WebGPU (Vue)', + emoji: '🎀', + colorFrom: 'gray', + colorTo: 'green', + sdk: 'static', + pinned: false, + license: 'mit', + models: ['onnx-community/whisper-base'], + short_description: 'Yet another Real-time Whisper with WebGPU, written in Vue', + thumbnail: 'https://raw.githubusercontent.com/moeru-ai/airi/refs/heads/main/packages/whisper-webgpu/public/banner.png' + }) + ] +}) +``` + +After bundling, a dedicated README with merged front-matter header will be generated in the root of your project: + +```md +--- +title: Real-time Whisper WebGPU (Vue) +emoji: 🎀 +colorFrom: gray +colorTo: green +sdk: static +pinned: false +license: mit +models: +- onnx-community/whisper-base +short_description: Yet another Real-time Whisper with WebGPU, written in Vue +thumbnail: https://raw.githubusercontent.com/moeru-ai/airi/refs/heads/main/packages/whisper-webgpu/public/banner.png +--- + +# Real-time Whisper WebGPU (Vue) +``` diff --git a/packages/hfsup/package.json b/packages/hfsup/package.json new file mode 100644 index 000000000..ac8bacf9d --- /dev/null +++ b/packages/hfsup/package.json @@ -0,0 +1,42 @@ +{ + "name": "hfsup", + "type": "module", + "private": false, + "description": "A collection of tools to help you deploy, bundle HuggingFace Spaces and related assets with ease.", + "author": { + "name": "Neko Ayaka", + "email": "neko@ayaka.moe", + "url": "https://github.com/nekomeowww" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/moeru-ai/airi.git", + "directory": "packages/huggingspace" + }, + "sideEffects": false, + "exports": { + "./vite": { + "types": "./dist/vite/index.d.ts", + "import": "./dist/vite/index.mjs", + "node": "./dist/vite/index.cjs" + } + }, + "main": "./dist/vite/index.cjs", + "module": "./dist/vite/index.mjs", + "types": "./dist/vite/index.d.ts", + "files": [ + "README.md", + "dist", + "package.json" + ], + "scripts": { + "dev": "pnpm run stub", + "stub": "unbuild --stub", + "build": "unbuild" + }, + "dependencies": { + "gray-matter": "^4.0.3", + "vite": "^6.0.5" + } +} diff --git a/packages/hfsup/src/utils/fs.ts b/packages/hfsup/src/utils/fs.ts new file mode 100644 index 000000000..808bf6190 --- /dev/null +++ b/packages/hfsup/src/utils/fs.ts @@ -0,0 +1,25 @@ +import { stat } from 'node:fs/promises' + +export async function exists(path: string) { + try { + await stat(path) + return true + } + catch (error) { + if (isENOENTError(error)) + return false + + throw error + } +} + +export function isENOENTError(error: unknown): boolean { + if (!(error instanceof Error)) + return false + if (!('code' in error)) + return false + if (error.code !== 'ENOENT') + return false + + return true +} diff --git a/packages/hfsup/src/vite/index.ts b/packages/hfsup/src/vite/index.ts new file mode 100644 index 000000000..1ad7a8e6a --- /dev/null +++ b/packages/hfsup/src/vite/index.ts @@ -0,0 +1,2 @@ +export * from './lfs' +export * from './space-card' diff --git a/packages/hfsup/src/vite/lfs.ts b/packages/hfsup/src/vite/lfs.ts new file mode 100644 index 000000000..972ebd974 --- /dev/null +++ b/packages/hfsup/src/vite/lfs.ts @@ -0,0 +1,74 @@ +import type { Plugin, ResolvedConfig } from 'vite' + +const defaultGitAttributes = `# Default +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text +` + +export function LFS(options?: { + enableFn?: (config: ResolvedConfig) => boolean | Promise + extraGlobs?: string[] + extraAttributes?: string[] + withDefault?: boolean +}): Plugin { + let _config: ResolvedConfig + + return { + name: 'huggingspace:lfs-gitattributes', + configResolved(config) { + _config = config + }, + async generateBundle() { + if (options?.enableFn && !(await options.enableFn(_config))) { + return + } + + const extraGlobs = options?.extraGlobs ?? [] + const extraAttributes = options?.extraAttributes ?? [] + const withDefault = options?.withDefault ?? true + const gitAttributes = withDefault ? defaultGitAttributes : '' + const extraGlobsIntoGitAttributes = extraGlobs.map((glob) => { + return `${glob} filter=lfs diff=lfs merge=lfs -text` + }) + + this.emitFile({ + type: 'asset', + fileName: '.gitattributes', + source: `${extraAttributes.join('\n')}${extraGlobsIntoGitAttributes.join('\n')}\n${gitAttributes}`, + }) + }, + } +} diff --git a/packages/hfsup/src/vite/space-card/index.ts b/packages/hfsup/src/vite/space-card/index.ts new file mode 100644 index 000000000..20e8c63b5 --- /dev/null +++ b/packages/hfsup/src/vite/space-card/index.ts @@ -0,0 +1,92 @@ +import type { Plugin, ResolvedConfig } from 'vite' +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import grayMatter from 'gray-matter' +import { exists } from '../../utils/fs' +import { type License, licenseValues, type SpaceConfiguration } from './types' + +// Please find the documentation at: +// Spaces Configuration Reference +// https://huggingface.co/docs/hub/spaces-config-reference +// https://huggingface.co/docs/hub/model-cards#model-card-metadata +export function SpaceCard(configuration?: SpaceConfiguration): Plugin { + let _config: ResolvedConfig + const _configuration: SpaceConfiguration = configuration ?? { emoji: 'πŸš€', sdk: 'static' } + let packageJSON: Record = {} + let readme = '' + + return { + name: 'huggingspace:readme', + async configResolved(config) { + _config = config + + const rootPackageJSONPath = join(_config.root, 'package.json') + if (await exists(rootPackageJSONPath)) { + const rootPackageJSONContent = await readFile(rootPackageJSONPath, 'utf-8') + packageJSON = JSON.parse(rootPackageJSONContent) + } + + const rootReadmePaths = [ + join(_config.root, 'README.md'), + join(_config.root, 'readme.md'), + join(_config.root, 'README.markdown'), + join(_config.root, 'readme.markdown'), + join(_config.root, 'README'), + join(_config.root, 'readme'), + ] + for (const rootReadmePath of rootReadmePaths) { + if (await exists(rootReadmePath)) { + const readReadme = await readFile(rootReadmePath, 'utf-8') + const { content } = grayMatter(readReadme) + readme = content + } + } + + if (_configuration.title == null) { + if (packageJSON == null) { + throw new Error(`\`title\` is required when \`package.json\` does not exist in the root directory (${rootPackageJSONPath})`) + } + if (!packageJSON.name) { + throw new Error(`\`title\` is required when \`name\` does not exist in the root package.json`) + } + + _configuration.title = packageJSON.name + } + + if (_configuration.license == null) { + if (packageJSON == null) { + throw new Error(`\`license\` is required when \`package.json\` does not exist in the root directory (${rootPackageJSONPath})`) + } + if (!packageJSON.license) { + throw new Error(`\`license\` is required when \`license\` does not exist in the root package.json`) + } + + const license = String(packageJSON.license).toLowerCase() + if (!(licenseValues.includes(license))) { + throw new Error(`Auto discovered license \`${license}\` in \`package.json\` is not supported. Must be one of licenses: ${licenseValues.join(', ')} to specified in \`configuration\` argument`) + } + + _configuration.license = license as License + } + + if (_configuration.sdk == null) { + _configuration.sdk = 'static' + } + + if (_configuration.short_description && _configuration.short_description.length > 60) { + throw new Error('short_description must be less or equal to 60 characters') + } + }, + async generateBundle() { + if (readme == null) { + readme = `# ${_configuration.title}` + } + + this.emitFile({ + type: 'asset', + fileName: 'README.md', + source: grayMatter.stringify({ content: `\n${readme}` }, _configuration), + }) + }, + } +} diff --git a/packages/hfsup/src/vite/space-card/types.ts b/packages/hfsup/src/vite/space-card/types.ts new file mode 100644 index 000000000..d9b34262d --- /dev/null +++ b/packages/hfsup/src/vite/space-card/types.ts @@ -0,0 +1,338 @@ +export const colorFromValues = ['red', 'yellow', 'green', 'blue', 'indigo', 'purple', 'pink', 'gray'] +export const colorToValues = ['red', 'yellow', 'green', 'blue', 'indigo', 'purple', 'pink', 'gray'] +export const sdkValues = ['gradio', 'streamlit', 'docker', 'static'] +export const suggestedStorageValues = ['small', 'medium', 'large'] +export const headerValues = ['mini', 'default'] +export const licenseValues = [ + 'apache-2.0', + 'mit', + 'openrail', + 'bigscience-openrail-m', + 'creativeml-openrail-m', + 'bigscience-bloom-rail-1.0', + 'bigcode-openrail-m', + 'afl-3.0', + 'artistic-2.0', + 'bsl-1.0', + 'bsd', + 'bsd-2-clause', + 'bsd-3-clause', + 'bsd-3-clause-clear', + 'c-uda', + 'cc', + 'cc0-1.0', + 'cc-by-2.0', + 'cc-by-2.5', + 'cc-by-3.0', + 'cc-by-4.0', + 'cc-by-sa-3.0', + 'cc-by-sa-4.0', + 'cc-by-nc-2.0', + 'cc-by-nc-3.0', + 'cc-by-nc-4.0', + 'cc-by-nd-4.0', + 'cc-by-nc-nd-3.0', + 'cc-by-nc-nd-4.0', + 'cc-by-nc-sa-2.0', + 'cc-by-nc-sa-3.0', + 'cc-by-nc-sa-4.0', + 'cdla-sharing-1.0', + 'cdla-permissive-1.0', + 'cdla-permissive-2.0', + 'wtfpl', + 'ecl-2.0', + 'epl-1.0', + 'epl-2.0', + 'etalab-2.0', + 'eupl-1.1', + 'agpl-3.0', + 'gfdl', + 'gpl', + 'gpl-2.0', + 'gpl-3.0', + 'lgpl', + 'lgpl-2.1', + 'lgpl-3.0', + 'isc', + 'lppl-1.3c', + 'ms-pl', + 'apple-ascl', + 'mpl-2.0', + 'odc-by', + 'odbl', + 'openrail++', + 'osl-3.0', + 'postgresql', + 'ofl-1.1', + 'ncsa', + 'unlicense', + 'zlib', + 'pddl', + 'lgpl-lr', + 'deepfloyd-if-license', + 'llama2', + 'llama3', + 'llama3.1', + 'llama3.2', + 'llama3.3', + 'gemma', + 'unknown', + 'other', +] + +export type License = 'apache-2.0' | 'mit' | 'openrail' | 'bigscience-openrail-m' | 'creativeml-openrail-m' + | 'bigscience-bloom-rail-1.0' | 'bigcode-openrail-m' | 'afl-3.0' | 'artistic-2.0' | 'bsl-1.0' | 'bsd' + | 'bsd-2-clause' | 'bsd-3-clause' | 'bsd-3-clause-clear' | 'c-uda' | 'cc' | 'cc0-1.0' | 'cc-by-2.0' + | 'cc-by-2.5' | 'cc-by-3.0' | 'cc-by-4.0' | 'cc-by-sa-3.0' | 'cc-by-sa-4.0' | 'cc-by-nc-2.0' + | 'cc-by-nc-3.0' | 'cc-by-nc-4.0' | 'cc-by-nd-4.0' | 'cc-by-nc-nd-3.0' | 'cc-by-nc-nd-4.0' + | 'cc-by-nc-sa-2.0' | 'cc-by-nc-sa-3.0' | 'cc-by-nc-sa-4.0' | 'cdla-sharing-1.0' | 'cdla-permissive-1.0' + | 'cdla-permissive-2.0' | 'wtfpl' | 'ecl-2.0' | 'epl-1.0' | 'epl-2.0' | 'etalab-2.0' | 'eupl-1.1' + | 'agpl-3.0' | 'gfdl' | 'gpl' | 'gpl-2.0' | 'gpl-3.0' | 'lgpl' | 'lgpl-2.1' | 'lgpl-3.0' | 'isc' + | 'lppl-1.3c' | 'ms-pl' | 'apple-ascl' | 'mpl-2.0' | 'odc-by' | 'odbl' | 'openrail++' | 'osl-3.0' + | 'postgresql' | 'ofl-1.1' | 'ncsa' | 'unlicense' | 'zlib' | 'pddl' | 'lgpl-lr' | 'deepfloyd-if-license' + | 'llama2' | 'llama3' | 'llama3.1' | 'llama3.2' | 'llama3.3' | 'gemma' | 'unknown' | 'other' + +export interface SpaceConfiguration { + /** + * Display title for the Space. + */ + title?: string + + /** + * License + * + * Model Cards + * https://huggingface.co/docs/hub/model-cards#model-card-metadata + */ + license?: License | License[] + + /** + * Space emoji (emoji-only character allowed). + */ + emoji?: string + + /** + * Color for Thumbnail gradient (red, yellow, green, blue, indigo, purple, pink, gray). + */ + colorFrom?: 'red' | 'yellow' | 'green' | 'blue' | 'indigo' | 'purple' | 'pink' | 'gray' + + /** + * Color for Thumbnail gradient (red, yellow, green, blue, indigo, purple, pink, gray). + */ + colorTo?: 'red' | 'yellow' | 'green' | 'blue' | 'indigo' | 'purple' | 'pink' | 'gray' + + /** + * Can be either gradio, streamlit, docker, or static. + */ + sdk?: 'gradio' | 'streamlit' | 'docker' | 'static' + + /** + * Any valid Python 3.x or 3.x.x version. + * Defaults to 3.10. + * + * @default "3.10" + */ + python_version?: string + + /** + * Specify the version of the selected SDK (Streamlit or Gradio). + * All versions of Gradio are supported. + * All versions of Streamlit from 0.79.0 are supported. + */ + sdk_version?: string + + /** + * Specify the suggested hardware on which this Space must be run. + * Useful for Spaces that are meant to be duplicated by other users. + * + * Setting this value will not automatically assign an hardware to this Space. + * + * Value must be a valid hardware flavor. Current valid hardware flavors: + * + * - CPU: `cpu-basic`, `cpu-upgrade` + * - GPU: `t4-small`, `t4-medium`, `l4x1`, `l4x4`, `a10g-small`, `a10g-large`, `a10g-largex2`, `a10g-largex4`,`a100-large` + * - TPU: `v5e-1x1`, `v5e-2x2`, `v5e-2x4` + */ + suggested_hardware?: string + + /** + * Specify the suggested permanent storage on which this Space must be run. + * Useful for Spaces that are meant to be duplicated by other users. + * Setting this value will not automatically assign a permanent storage to this Space. + * Value must be one of "small", "medium" or "large". + */ + suggested_storage?: 'small' | 'medium' | 'large' + + /** + * Path to your main application file (which contains either gradio or streamlit Python code, or static html code). + * Path is relative to the root of the repository. + * + * @default "index.html" + */ + app_file?: string + + /** + * Port on which your application is running. Used only if sdk is docker. + * + * Default port is 7860. + * + * @default 7860 + */ + app_port?: number + + /** + * For non-static Spaces, initial url to render. + * Needs to start with /. + * For static Spaces, use app_file instead. + */ + base_path?: string + + /** + * Whether your Space is rendered inside a full-width (when true) or fixed-width column (ie. β€œcontainer” CSS) inside the iframe. + * Defaults to true. + * + * @default true + */ + fullWidth?: boolean + + /** + * Can be either mini or default. If header is set to mini the space + * will be displayed full-screen with a mini floating header . + */ + header?: 'mini' | 'default' + + /** + * A short description of the Space. This will be displayed in the Space’s thumbnail. + * + * Must less or equal to 60 characters. + */ + short_description?: string + + /** + * HF model IDs (like `openai-community/gpt2` or `deepset/roberta-base-squad2`) used in + * the Space. + * + * Will be parsed automatically from your code if not specified here. + * + * ```yaml + * models: + * - openai-community/gpt2 + * - deepset/roberta-base-squad2 + * ``` + */ + models?: string[] + + /** + * HF dataset IDs (like `mozilla-foundation/common_voice_13_0` or `oscar-corpus/OSCAR-2109`) + * used in the Space. + * + * Will be parsed automatically from your code if not specified here. + * + * ```yaml + * datasets: + * - mozilla-foundation/common_voice_13_0 + * - oscar-corpus/OSCAR-2109 + * ``` + */ + datasets?: string[] + + /** + * List of terms that describe your Space task or scope. + */ + tags?: string[] + + /** + * URL for defining a custom thumbnail for social sharing. + */ + thumbnail?: string + + /** + * Whether the Space stays on top of your profile. + * + * Can be useful if you have a lot of Spaces so you and others can quickly see your best Space. + * + * @default false + */ + pinned?: boolean + + /** + * Whether a connected OAuth app is associated to this Space. See Adding a + * Sign-In with HF button to your Space for more details. + * + * @default false + */ + hf_oauth?: boolean + + /** + * Authorized scopes of the connected OAuth app. `openid` and `profile` are authorized + * by default and do not need this parameter. See Adding a Sign-In + * with HF button to your space for more details. + * + * @default ["openid","profile"] + */ + hf_oauth_scopes?: string[] + + /** + * Duration of the OAuth token in minutes. + * + * Defaults to 480 minutes (8 hours). Maximum duration is 43200 minutes (30 days). + * See Adding a Sign-In with HF button to your space for more details. + * + * @default 480 + */ + hf_oauth_expiration_minutes?: number + + /** + * Whether the Space iframe can be embedded in other websites. + * + * Defaults to false, i.e. Spaces can be embedded. + * + * @default false + */ + disable_embedding?: boolean + + /** + * Set a custom startup duration timeout for your Space. + * + * This is the maximum time your Space is allowed to start before it times out + * and is flagged as unhealthy. + * + * Defaults to 30 minutes, but any valid duration (like 1h, 30m) is acceptable. + * + * @default "30m" + */ + startup_duration_timeout?: string + + /** + * Set custom HTTP headers that will be added to all HTTP responses when serving your Space. + * For now, only the cross-origin-embedder-policy (COEP), cross-origin-opener-policy (COOP), + * and cross-origin-resource-policy (CORP) headers are allowed. + * These headers can be used to set up a cross-origin isolated environment and enable powerful + * features like SharedArrayBuffer, for example: + * + * ```yaml + * custom_headers: + * cross-origin-embedder-policy: require-corp + * cross-origin-opener-policy: same-origin + * cross-origin-resource-policy: cross-origin + * ``` + * + * Note: all headers and values must be lowercase. + */ + custom_headers?: Record + + /** + * Specify a list of Hugging Face Hub models or other large files to be preloaded during the build + * time of your Space. + * + * This optimizes the startup time by having the files ready when your application starts. + * This is particularly useful for Spaces that rely on large models or datasets that would otherwise + * need to be downloaded at runtime. + * + * The format for each item is "repository_name" to download all files from a repository, or "repository_name file1,file2" + * for downloading specific files within that repository. + * + * You can also specify a specific commit to download using the format "repository_name file1,file2 commit_sha256". + */ + preload_from_hub?: string[] +} diff --git a/packages/hfsup/tsconfig.json b/packages/hfsup/tsconfig.json new file mode 100644 index 000000000..c84f5b094 --- /dev/null +++ b/packages/hfsup/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + } +} diff --git a/packages/moonshine-web/README.md b/packages/moonshine-web/README.md index 8292db974..1a6398b77 100644 --- a/packages/moonshine-web/README.md +++ b/packages/moonshine-web/README.md @@ -1,17 +1,3 @@ ---- -title: Moonshine Web (Vue) -emoji: πŸŒ™ -colorFrom: blue -colorTo: pink -sdk: static -pinned: false -license: mit -models: - - onnx-community/moonshine-base-ONNX -short_description: Yet another Real-time in-browser STT, re-implemented in Vue -thumbnail: https://raw.githubusercontent.com/moeru-ai/airi/refs/heads/main/packages/moonshine-web/public/banner.png ---- -

Moonshine Web (Vue)

diff --git a/packages/moonshine-web/package.json b/packages/moonshine-web/package.json index d28ae7535..9ccebe96b 100644 --- a/packages/moonshine-web/package.json +++ b/packages/moonshine-web/package.json @@ -32,6 +32,7 @@ "@types/three": "^0.171.0", "@vitejs/plugin-vue": "^5.2.1", "@webgpu/types": "^0.1.52", + "hfsup": "workspace:^", "vue-tsc": "^2.2.0" } } diff --git a/packages/moonshine-web/vite.config.ts b/packages/moonshine-web/vite.config.ts index abb1ece6b..1af3182de 100644 --- a/packages/moonshine-web/vite.config.ts +++ b/packages/moonshine-web/vite.config.ts @@ -1,5 +1,6 @@ import { templateCompilerOptions } from '@tresjs/core' import Vue from '@vitejs/plugin-vue' +import { LFS, SpaceCard } from 'hfsup/vite' import Unocss from 'unocss/vite' import { defineConfig } from 'vite' @@ -12,6 +13,21 @@ export default defineConfig({ // https://github.com/antfu/unocss // see uno.config.ts for config Unocss(), + + // HuggingFace Spaces + LFS(), + SpaceCard({ + title: 'Moonshine Web (Vue)', + emoji: 'πŸŒ™', + colorFrom: 'blue', + colorTo: 'pink', + sdk: 'static', + pinned: false, + license: 'mit', + models: ['onnx-community/moonshine-base-ONNX'], + short_description: 'Yet another Real-time in-browser STT, re-implemented in Vue', + thumbnail: 'https://raw.githubusercontent.com/moeru-ai/airi/refs/heads/main/packages/moonshine-web/public/banner.png', + }), ], worker: { format: 'es' }, }) diff --git a/packages/stage/README.md b/packages/stage/README.md index 8bd1bcc9f..3233e4cf2 100644 --- a/packages/stage/README.md +++ b/packages/stage/README.md @@ -1,13 +1,7 @@ ---- -title: γ‚’γ‚€γƒͺ VTuber -emoji: πŸŒ™ -colorFrom: pink -colorTo: yellow -sdk: static -pinned: false -license: mit -models: - - onnx-community/whisper-base - - -short_description: γ‚’γ‚€γƒͺ VTuber. LLM powered Live2D/VRM living character. ---- +

γ‚’γ‚€γƒͺ VTuber

+ +

+ [Try it] +

+ +> Heavily inspired by [Neuro-sama](https://www.youtube.com/@Neurosama) diff --git a/packages/stage/package.json b/packages/stage/package.json index e9cbded88..2ad983d2d 100644 --- a/packages/stage/package.json +++ b/packages/stage/package.json @@ -88,6 +88,7 @@ "@vitejs/plugin-vue": "^5.2.1", "@vue-macros/volar": "^0.30.8", "@vueuse/motion": "^2.2.6", + "hfsup": "workspace:^", "markdown-it-link-attributes": "^4.0.1", "unocss-preset-theme": "^0.14.1", "unplugin-auto-import": "^0.19.0", diff --git a/packages/stage/src/components/Widgets/Stage.vue b/packages/stage/src/components/Widgets/Stage.vue index d54dfb245..6bb9f8dda 100644 --- a/packages/stage/src/components/Widgets/Stage.vue +++ b/packages/stage/src/components/Widgets/Stage.vue @@ -175,14 +175,14 @@ onUnmounted(() => { ref="live2DViewerRef" :mouth-open-size="mouthOpenSize" model="/assets/live2d/models/hiyori_pro_zh/runtime/hiyori_pro_t11.model3.json" - w="50%" min-w="50% diff --git a/packages/stage/src/main.ts b/packages/stage/src/main.ts index 5e5038924..7ba8bcabf 100644 --- a/packages/stage/src/main.ts +++ b/packages/stage/src/main.ts @@ -1,3 +1,4 @@ +import type { Router } from 'vue-router' import { autoAnimatePlugin } from '@formkit/auto-animate/vue' import Tres from '@tresjs/core' import { MotionPlugin } from '@vueuse/motion' @@ -5,7 +6,7 @@ import NProgress from 'nprogress' import { createPinia } from 'pinia' import { setupLayouts } from 'virtual:generated-layouts' import { createApp } from 'vue' -import { createRouter, createWebHistory } from 'vue-router' +import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router' import { routes } from 'vue-router/auto-routes' @@ -16,7 +17,13 @@ import './styles/main.css' import 'uno.css' const pinia = createPinia() -const router = createRouter({ routes: setupLayouts(routes), history: createWebHistory() }) +const routeRecords = setupLayouts(routes) + +let router: Router +if (import.meta.env.VITE_APP_TARGET_HUGGINGFACE_SPACE) + router = createRouter({ routes: routeRecords, history: createWebHashHistory() }) +else + router = createRouter({ routes: routeRecords, history: createWebHistory() }) router.beforeEach((to, from) => { if (to.path !== from.path) @@ -29,10 +36,14 @@ router.afterEach(() => { router.isReady() .then(async () => { + if (import.meta.env.VITE_APP_TARGET_HUGGINGFACE_SPACE) { + return + } + const { registerSW } = await import('virtual:pwa-register') registerSW({ immediate: true }) }) - .catch(() => { }) + .catch(() => {}) createApp(App) .use(MotionPlugin) diff --git a/packages/stage/src/pages/[...all].vue b/packages/stage/src/pages/[...all].vue new file mode 100644 index 000000000..6bda8c141 --- /dev/null +++ b/packages/stage/src/pages/[...all].vue @@ -0,0 +1,5 @@ + diff --git a/packages/stage/src/pages/index.vue b/packages/stage/src/pages/index.vue index fd8b7d341..90aa2f286 100644 --- a/packages/stage/src/pages/index.vue +++ b/packages/stage/src/pages/index.vue @@ -8,8 +8,8 @@ import Stage from '../components/Widgets/Stage.vue'
- - + +
diff --git a/packages/stage/vite-env.d.ts b/packages/stage/vite-env.d.ts new file mode 100644 index 000000000..76326b3ee --- /dev/null +++ b/packages/stage/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_APP_TARGET_HUGGINGFACE_SPACE: boolean + // more env variables... +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/packages/stage/vite.config.ts b/packages/stage/vite.config.ts index 7e2d4256f..7cda2740b 100644 --- a/packages/stage/vite.config.ts +++ b/packages/stage/vite.config.ts @@ -2,9 +2,11 @@ import { Buffer } from 'node:buffer' import { copyFile, cp, mkdir, readFile, writeFile } from 'node:fs/promises' import path, { join, resolve } from 'node:path' +import { env } from 'node:process' import VueI18n from '@intlify/unplugin-vue-i18n/vite' import { templateCompilerOptions } from '@tresjs/core' import Vue from '@vitejs/plugin-vue' +import { LFS, SpaceCard } from 'hfsup/vite' import { ofetch } from 'ofetch' import Unocss from 'unocss/vite' import Components from 'unplugin-vue-components/vite' @@ -13,13 +15,21 @@ import VueRouter from 'unplugin-vue-router/vite' import { defineConfig } from 'vite' import { VitePWA } from 'vite-plugin-pwa' import VueDevTools from 'vite-plugin-vue-devtools' -import Layouts from 'vite-plugin-vue-layouts' +import Layouts from 'vite-plugin-vue-layouts' import { exists } from './scripts/fs' import { unzip } from './scripts/unzip' import { appName } from './src/constants' export default defineConfig({ + build: { + rollupOptions: { + external: [ + 'virtual:pwa-register', + ], + }, + }, + optimizeDeps: { exclude: [ 'public/assets/*', @@ -73,27 +83,29 @@ export default defineConfig({ Unocss(), // https://github.com/antfu/vite-plugin-pwa - VitePWA({ - registerType: 'autoUpdate', - includeAssets: ['favicon.svg', 'apple-touch-icon.png'], - manifest: { - name: appName, - short_name: appName, - theme_color: '#ffffff', - icons: [ - { - src: '/web-app-manifest-192x192.png', - sizes: '192x192', - type: 'image/png', + ...(env.TARGET_HUGGINGFACE_SPACE + ? [] + : [VitePWA({ + registerType: 'autoUpdate', + includeAssets: ['favicon.svg', 'apple-touch-icon.png'], + manifest: { + name: appName, + short_name: appName, + theme_color: '#ffffff', + icons: [ + { + src: '/web-app-manifest-192x192.png', + sizes: '192x192', + type: 'image/png', + }, + { + src: '/web-app-manifest-512x512.png', + sizes: '512x512', + type: 'image/png', + }, + ], }, - { - src: '/web-app-manifest-512x512.png', - sizes: '512x512', - type: 'image/png', - }, - ], - }, - }), + })]), // https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n VueI18n({ @@ -269,5 +281,30 @@ export default defineConfig({ } }, }, + + // HuggingFace Spaces + LFS({ + extraGlobs: [ + '*.vrm', + '*.cmo3', + '*.png', + '*.jpg', + '*.jpeg', + '*.gif', + '*.webp', + '*.bmp', + ], + }), + SpaceCard({ + title: 'γ‚’γ‚€γƒͺ VTuber', + emoji: 'πŸŒ™', + colorFrom: 'pink', + colorTo: 'yellow', + sdk: 'static', + pinned: false, + license: 'mit', + models: ['onnx-community/whisper-base'], + short_description: 'γ‚’γ‚€γƒͺ VTuber. LLM powered Live2D/VRM living character.', + }), ], }) diff --git a/packages/whisper-webgpu/README.md b/packages/whisper-webgpu/README.md index f2590cbd3..654d97bf2 100644 --- a/packages/whisper-webgpu/README.md +++ b/packages/whisper-webgpu/README.md @@ -1,17 +1,3 @@ ---- -title: Real-time Whisper WebGPU (Vue) -emoji: 🎀 -colorFrom: gray -colorTo: green -sdk: static -pinned: false -license: mit -models: -- onnx-community/whisper-base -short_description: Yet another Real-time Whisper with WebGPU, written in Vue -thumbnail: https://raw.githubusercontent.com/moeru-ai/airi/refs/heads/main/packages/whisper-webgpu/public/banner.png ---- -

Whisper Realtime Demo (WebGPU)

diff --git a/packages/whisper-webgpu/package.json b/packages/whisper-webgpu/package.json index a6de14fa5..34802dbee 100644 --- a/packages/whisper-webgpu/package.json +++ b/packages/whisper-webgpu/package.json @@ -27,6 +27,7 @@ "@huggingface/transformers": "^3.2.3", "@vitejs/plugin-vue": "^5.2.1", "@webgpu/types": "^0.1.52", + "hfsup": "workspace:^", "vue-tsc": "^2.2.0" } } diff --git a/packages/whisper-webgpu/vite.config.ts b/packages/whisper-webgpu/vite.config.ts index adf843606..caa132273 100644 --- a/packages/whisper-webgpu/vite.config.ts +++ b/packages/whisper-webgpu/vite.config.ts @@ -1,4 +1,5 @@ import Vue from '@vitejs/plugin-vue' +import { LFS, SpaceCard } from 'hfsup/vite' import Unocss from 'unocss/vite' import { defineConfig } from 'vite' @@ -8,5 +9,20 @@ export default defineConfig({ // https://github.com/antfu/unocss // see uno.config.ts for config Unocss(), + + // HuggingFace Spaces + LFS(), + SpaceCard({ + title: 'Real-time Whisper WebGPU (Vue)', + emoji: '🎀', + colorFrom: 'gray', + colorTo: 'green', + sdk: 'static', + pinned: false, + license: 'mit', + models: ['onnx-community/whisper-base'], + short_description: 'Yet another Real-time Whisper with WebGPU, written in Vue', + thumbnail: 'https://raw.githubusercontent.com/moeru-ai/airi/refs/heads/main/packages/whisper-webgpu/public/banner.png', + }), ], }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3fb3adb76..0ec229af3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,15 @@ importers: specifier: ^1.50.2 version: 1.50.2 + packages/hfsup: + dependencies: + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 + vite: + specifier: ^6.0.5 + version: 6.0.5(@types/node@22.10.2)(jiti@2.4.0)(terser@5.17.6)(tsx@4.19.2)(yaml@2.6.1) + packages/llm-proxy: dependencies: elevenlabs: @@ -141,6 +150,9 @@ importers: '@webgpu/types': specifier: ^0.1.52 version: 0.1.52 + hfsup: + specifier: workspace:^ + version: link:../hfsup vue-tsc: specifier: ^2.2.0 version: 2.2.0(typescript@5.7.2) @@ -352,6 +364,9 @@ importers: '@vueuse/motion': specifier: ^2.2.6 version: 2.2.6(rollup@2.79.1)(vue@3.5.13(typescript@5.7.2)) + hfsup: + specifier: workspace:^ + version: link:../hfsup markdown-it-link-attributes: specifier: ^4.0.1 version: 4.0.1 @@ -416,6 +431,9 @@ importers: '@webgpu/types': specifier: ^0.1.52 version: 0.1.52 + hfsup: + specifier: workspace:^ + version: link:../hfsup vue-tsc: specifier: ^2.2.0 version: 2.2.0(typescript@5.7.2) @@ -1934,14 +1952,14 @@ packages: resolution: {integrity: sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==} engines: {node: '>= 16'} - '@intlify/shared@11.0.0': - resolution: {integrity: sha512-MLKIyFz37qYF1G5UQZMwGoJyp8DD3rtcXDplRHZdSOmbfOYbD4g9kJZ6qCsm1Ru3vee6CTnJcbO4LV1JeIDGbA==} - engines: {node: '>= 16'} - '@intlify/shared@11.0.0-rc.1': resolution: {integrity: sha512-8tR1xe7ZEbkabTuE/tNhzpolygUn9OaYp9yuYAF4MgDNZg06C3Qny80bes2/e9/Wm3aVkPUlCw6WgU7mQd0yEg==} engines: {node: '>= 16'} + '@intlify/shared@11.0.1': + resolution: {integrity: sha512-lH164+aDDptHZ3dBDbIhRa1dOPQUp+83iugpc+1upTOWCnwyC1PVis6rSWNMMJ8VQxvtHQB9JMib48K55y0PvQ==} + engines: {node: '>= 16'} + '@intlify/unplugin-vue-i18n@6.0.2': resolution: {integrity: sha512-5AuwBfdFtA4OSh31tfGpjhvmh7BVCzClHCgFlVdNM3ojyai3UZY4ViYh35TlE1xeWFyduOtUsG12r70xy24nzg==} engines: {node: '>= 18'} @@ -8882,16 +8900,16 @@ snapshots: '@intlify/shared@10.0.5': {} - '@intlify/shared@11.0.0': {} - '@intlify/shared@11.0.0-rc.1': {} + '@intlify/shared@11.0.1': {} + '@intlify/unplugin-vue-i18n@6.0.2(@vue/compiler-dom@3.5.13)(eslint@9.17.0(jiti@2.4.0))(rollup@2.79.1)(typescript@5.7.2)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))': dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@2.4.0)) '@intlify/bundle-utils': 10.0.0(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2))) - '@intlify/shared': 11.0.0 - '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.0.0)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2)) + '@intlify/shared': 11.0.1 + '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.0.1)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2)) '@rollup/pluginutils': 5.1.3(rollup@2.79.1) '@typescript-eslint/scope-manager': 8.18.0 '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.7.2) @@ -8913,11 +8931,11 @@ snapshots: - supports-color - typescript - '@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.0.0)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))': + '@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.0.1)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))': dependencies: '@babel/parser': 7.26.2 optionalDependencies: - '@intlify/shared': 11.0.0 + '@intlify/shared': 11.0.1 '@vue/compiler-dom': 3.5.13 vue: 3.5.13(typescript@5.7.2) vue-i18n: 10.0.5(vue@3.5.13(typescript@5.7.2))