@webship.co/workspace 1.0.0-beta1 → 1.0.0-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +339 -0
- package/README.md +28 -364
- package/core/bin/workspace.js +135 -0
- package/core/config/settings.labels.yml +514 -0
- package/core/config/settings.template.yml +292 -0
- package/core/config/style.yml +53 -0
- package/core/config/theme.default.yml +161 -0
- package/core/config/theme.dispatch.yml +166 -0
- package/core/config/theme.drupal.yml +175 -0
- package/core/config/theme.ember.yml +160 -0
- package/core/config/theme.github.yml +172 -0
- package/core/config/theme.gitlab.yml +168 -0
- package/core/config/theme.press.yml +161 -0
- package/core/config/theme.slate.yml +160 -0
- package/core/config/theme.upsun.yml +174 -0
- package/core/docs/architecture.svg +65 -0
- package/core/docs/getting-started.md +51 -0
- package/core/docs/plugins.md +27 -0
- package/core/docs/releases.md +23 -0
- package/core/docs/testing.md +15 -0
- package/core/docs/workspaces.md +25 -0
- package/core/scripts/args/arg-ai-items.sh +65 -0
- package/core/scripts/args/arg-autostart.sh +31 -0
- package/core/scripts/args/arg-avatar-agent.sh +28 -0
- package/core/scripts/args/arg-avatar-catalog.sh +12 -0
- package/core/scripts/args/arg-avatar-character.sh +68 -0
- package/core/scripts/args/arg-avatar-voice.sh +24 -0
- package/core/scripts/args/arg-avatar.sh +22 -0
- package/core/scripts/args/arg-checks.sh +36 -0
- package/core/scripts/args/arg-clone.sh +42 -0
- package/core/scripts/args/arg-commands.sh +51 -0
- package/core/scripts/args/arg-cucumber.sh +34 -0
- package/core/scripts/args/arg-distribution.sh +30 -0
- package/core/scripts/args/arg-drupal.sh +39 -0
- package/core/scripts/args/arg-drupal_cms.sh +18 -0
- package/core/scripts/args/arg-gitlab-ci-local.sh +25 -0
- package/core/scripts/args/arg-graphify.sh +36 -0
- package/core/scripts/args/arg-graphs.sh +12 -0
- package/core/scripts/args/arg-hostnames.sh +23 -0
- package/core/scripts/args/arg-milvus-backup.sh +8 -0
- package/core/scripts/args/arg-milvus-mcp.sh +12 -0
- package/core/scripts/args/arg-milvus.sh +32 -0
- package/core/scripts/args/arg-obsidian.sh +17 -0
- package/core/scripts/args/arg-odoo.sh +18 -0
- package/core/scripts/args/arg-penpot-backup.sh +8 -0
- package/core/scripts/args/arg-penpot-mcp.sh +15 -0
- package/core/scripts/args/arg-penpot-token.sh +17 -0
- package/core/scripts/args/arg-penpot.sh +44 -0
- package/core/scripts/args/arg-rag.sh +21 -0
- package/core/scripts/args/arg-ragify.sh +35 -0
- package/core/scripts/args/arg-reclaim.sh +18 -0
- package/core/scripts/args/arg-release.sh +21 -0
- package/core/scripts/args/arg-screen.sh +28 -0
- package/core/scripts/args/arg-site-template.sh +13 -0
- package/core/scripts/args/arg-spdd.sh +16 -0
- package/core/scripts/args/arg-specs.sh +22 -0
- package/core/scripts/args/arg-storage.sh +36 -0
- package/core/scripts/args/arg-testing.sh +36 -0
- package/core/scripts/args/arg-webship.sh +34 -0
- package/core/scripts/args/arg-webships-project.sh +22 -0
- package/core/scripts/args/arg-webships-site-template.sh +17 -0
- package/core/scripts/args/arg-webships.sh +34 -0
- package/core/scripts/args/arg-website-site-template.sh +17 -0
- package/core/scripts/args/arg-website.sh +25 -0
- package/core/scripts/args/arg-wordpress.sh +18 -0
- package/core/scripts/autostart-run.sh +162 -0
- package/core/scripts/autostart-stop.sh +30 -0
- package/core/scripts/autostart-watch.sh +43 -0
- package/core/scripts/autostart.sh +26 -0
- package/core/scripts/bootstrap-functions.sh +117 -0
- package/core/scripts/bootstrap-libraries.sh +10 -0
- package/core/scripts/bootstrap.sh +71 -0
- package/core/scripts/ddev-shim/ddev +122 -0
- package/core/scripts/docker-gid.sh +119 -0
- package/core/scripts/functions/arg-site-template.sh +28 -0
- package/core/scripts/functions/fun-ai-items-lock.sh +175 -0
- package/core/scripts/functions/fun-ai-items-sources.sh +298 -0
- package/core/scripts/functions/fun-ai-items.sh +621 -0
- package/core/scripts/functions/fun-ai-providers.sh +72 -0
- package/core/scripts/functions/fun-autostart.sh +429 -0
- package/core/scripts/functions/fun-avatar.sh +441 -0
- package/core/scripts/functions/fun-backup-item.sh +50 -0
- package/core/scripts/functions/fun-build-distribution.sh +226 -0
- package/core/scripts/functions/fun-build-site-template.sh +104 -0
- package/core/scripts/functions/fun-build-webships.sh +161 -0
- package/core/scripts/functions/fun-build-website.sh +199 -0
- package/core/scripts/functions/fun-checks.sh +243 -0
- package/core/scripts/functions/fun-clone-project.sh +289 -0
- package/core/scripts/functions/fun-commands.sh +521 -0
- package/core/scripts/functions/fun-database.sh +63 -0
- package/core/scripts/functions/fun-ddev-hostnames.sh +213 -0
- package/core/scripts/functions/fun-ddev-nodejs.sh +15 -0
- package/core/scripts/functions/fun-ddev-telemetry.sh +15 -0
- package/core/scripts/functions/fun-default-settings.sh +28 -0
- package/core/scripts/functions/fun-distribution-cucumber.sh +45 -0
- package/core/scripts/functions/fun-distribution-drupal.sh +25 -0
- package/core/scripts/functions/fun-distribution-drupal_cms.sh +35 -0
- package/core/scripts/functions/fun-distribution-webship.sh +28 -0
- package/core/scripts/functions/fun-distribution-webships.sh +33 -0
- package/core/scripts/functions/fun-distribution-website.sh +19 -0
- package/core/scripts/functions/fun-drush.sh +48 -0
- package/core/scripts/functions/fun-elevenlabs.sh +179 -0
- package/core/scripts/functions/fun-files.sh +19 -0
- package/core/scripts/functions/fun-git-worktrees.sh +142 -0
- package/core/scripts/functions/fun-gitlab-ci.sh +156 -0
- package/core/scripts/functions/fun-hostnames-cli.sh +88 -0
- package/core/scripts/functions/fun-milvus.sh +584 -0
- package/core/scripts/functions/fun-odoo.sh +121 -0
- package/core/scripts/functions/fun-penpot.sh +724 -0
- package/core/scripts/functions/fun-project-name.sh +20 -0
- package/core/scripts/functions/fun-rag.sh +253 -0
- package/core/scripts/functions/fun-ragify.sh +165 -0
- package/core/scripts/functions/fun-reclaim.sh +297 -0
- package/core/scripts/functions/fun-release.sh +159 -0
- package/core/scripts/functions/fun-remove-project.sh +319 -0
- package/core/scripts/functions/fun-screen.sh +96 -0
- package/core/scripts/functions/fun-spdd.sh +676 -0
- package/core/scripts/functions/fun-specs-share.sh +237 -0
- package/core/scripts/functions/fun-storage.sh +347 -0
- package/core/scripts/functions/fun-testing.sh +329 -0
- package/core/scripts/functions/fun-users.sh +87 -0
- package/core/scripts/functions/fun-version.sh +72 -0
- package/core/scripts/functions/fun-video-edit.sh +48 -0
- package/core/scripts/functions/fun-voice.sh +45 -0
- package/core/scripts/functions/fun-wordpress.sh +59 -0
- package/core/scripts/host-mounts.sh +161 -0
- package/core/scripts/install.sh +602 -0
- package/core/scripts/libs/__pycache__/video_edit.cpython-312.pyc +0 -0
- package/core/scripts/libs/argparse.sh +93 -0
- package/core/scripts/libs/bash-yaml.sh +52 -0
- package/core/scripts/libs/checks_plan.js +312 -0
- package/core/scripts/libs/elevenlabs.py +627 -0
- package/core/scripts/libs/progress-bar.sh +19 -0
- package/core/scripts/libs/rag_index.py +446 -0
- package/core/scripts/libs/video_edit.py +636 -0
- package/core/scripts/libs/voice.py +430 -0
- package/core/scripts/plugins/jira/host-install.sh +119 -0
- package/core/scripts/plugins/jira/host-mounts.txt +2 -0
- package/core/scripts/plugins/upsun/args/arg-upsun.sh +51 -0
- package/core/scripts/plugins/upsun/functions/fun-upsun.sh +304 -0
- package/core/scripts/plugins/upsun/host-install.sh +99 -0
- package/core/scripts/plugins/upsun/host-mounts.txt +1 -0
- package/core/scripts/plugins/upsun/upsun-ai.sh +133 -0
- package/core/scripts/plugins.sh +152 -0
- package/core/scripts/release.sh +69 -0
- package/core/scripts/storage.sh +39 -0
- package/core/scripts/sync-tooling.sh +246 -0
- package/core/scripts/tests/cmd-smoke-test.sh +109 -0
- package/core/scripts/trust-https.sh +257 -0
- package/core/workspace-app/.ddev/commands/web/test-dashboard +49 -0
- package/core/workspace-app/.ddev/config.yaml +400 -0
- package/core/workspace-app/.ddev/docker-compose.dood.yaml +45 -0
- package/core/workspace-app/.ddev/docker-compose.plugin-jira-twg.yaml +26 -0
- package/core/workspace-app/.ddev/docker-compose.plugin-upsun-upsun-cli.yaml +8 -0
- package/core/workspace-app/.ddev/docker-compose.screens.yaml +15 -0
- package/core/workspace-app/.ddev/nginx_full/nginx-site.conf +114 -0
- package/core/workspace-app/.ddev/web-build/Dockerfile.workspace-app +203 -0
- package/core/workspace-app/.ddev/web-build/ddev-shim-ddev +122 -0
- package/{PLUGINS.md → core/workspace-app/PLUGINS.md} +9 -1
- package/core/workspace-app/README.md +380 -0
- package/{actions → core/workspace-app/actions}/ddev.js +0 -1
- package/{actions → core/workspace-app/actions}/rag.js +0 -1
- package/{actions → core/workspace-app/actions}/testing.js +0 -1
- package/{assistant.js → core/workspace-app/assistant.js} +1 -10
- package/{git-info.js → core/workspace-app/git-info.js} +17 -1
- package/core/workspace-app/package.json +29 -0
- package/core/workspace-app/plugin-catalog.js +50 -0
- package/{plugins-page.js → core/workspace-app/plugins-page.js} +28 -2
- package/{server.js → core/workspace-app/server.js} +18 -0
- package/core/workspace-app/tests/features/actions-rail.feature +44 -0
- package/core/workspace-app/tests/features/ai-items.feature +23 -0
- package/core/workspace-app/tests/features/assistant-speech.feature +63 -0
- package/core/workspace-app/tests/features/checks.feature +40 -0
- package/core/workspace-app/tests/features/clone-project.feature +30 -0
- package/core/workspace-app/tests/features/command-authoring.feature +58 -0
- package/core/workspace-app/tests/features/dashboard.feature +57 -0
- package/core/workspace-app/tests/features/menus.feature +29 -0
- package/core/workspace-app/tests/features/plugins/jira/jira.feature +47 -0
- package/core/workspace-app/tests/features/plugins.feature +20 -0
- package/core/workspace-app/tests/features/project-actions.feature +41 -0
- package/core/workspace-app/tests/features/screens.feature +29 -0
- package/core/workspace-app/tests/features/settings.feature +46 -0
- package/core/workspace-app/tests/features/test-reports.feature +32 -0
- package/core/workspace-app/tests/features/theme.feature +40 -0
- package/core/workspace-app/tests/step-definitions/actions-post.steps.js +25 -0
- package/core/workspace-app/tests/step-definitions/assistant-speech.steps.js +157 -0
- package/core/workspace-app/tests/step-definitions/clone-project.steps.js +47 -0
- package/core/workspace-app/tests/step-definitions/preconditions.steps.js +168 -0
- package/core/workspace-app/tests/unit/_isolated-config.js +31 -0
- package/core/workspace-app/tests/unit/accessibility.test.js +124 -0
- package/core/workspace-app/tests/unit/actions-rail-ux.test.js +56 -0
- package/core/workspace-app/tests/unit/actions-rail.test.js +51 -0
- package/core/workspace-app/tests/unit/ai-inventory.test.js +64 -0
- package/core/workspace-app/tests/unit/app-docs.test.js +64 -0
- package/core/workspace-app/tests/unit/assistant-avatar.test.js +110 -0
- package/core/workspace-app/tests/unit/assistant-cast.test.js +105 -0
- package/core/workspace-app/tests/unit/assistant-send-animation.test.js +15 -0
- package/core/workspace-app/tests/unit/assistant-suggestions.test.js +112 -0
- package/core/workspace-app/tests/unit/assistant-switches.test.js +53 -0
- package/core/workspace-app/tests/unit/assistant-views.test.js +146 -0
- package/core/workspace-app/tests/unit/assistant-working-indicator.test.js +49 -0
- package/core/workspace-app/tests/unit/assistant.test.js +61 -0
- package/core/workspace-app/tests/unit/audio-player.test.js +147 -0
- package/core/workspace-app/tests/unit/audio-voice.test.js +634 -0
- package/core/workspace-app/tests/unit/auth-google.test.js +180 -0
- package/core/workspace-app/tests/unit/autostart-watch.test.js +76 -0
- package/core/workspace-app/tests/unit/browsers.test.js +76 -0
- package/core/workspace-app/tests/unit/build-distribution.test.js +125 -0
- package/core/workspace-app/tests/unit/build-order.test.js +153 -0
- package/core/workspace-app/tests/unit/builder-args.test.js +113 -0
- package/core/workspace-app/tests/unit/builders.test.js +158 -0
- package/core/workspace-app/tests/unit/card-state-toggle.test.js +29 -0
- package/core/workspace-app/tests/unit/changelog.test.js +223 -0
- package/core/workspace-app/tests/unit/chat-thread.test.js +92 -0
- package/core/workspace-app/tests/unit/checks-plan.test.js +205 -0
- package/core/workspace-app/tests/unit/claude-args.test.js +182 -0
- package/core/workspace-app/tests/unit/claude-code-here.test.js +15 -0
- package/core/workspace-app/tests/unit/claude-resume.test.js +147 -0
- package/core/workspace-app/tests/unit/claude-screen-options.test.js +84 -0
- package/core/workspace-app/tests/unit/claude-session.test.js +41 -0
- package/core/workspace-app/tests/unit/command-flags.test.js +45 -0
- package/core/workspace-app/tests/unit/command-palette.test.js +105 -0
- package/core/workspace-app/tests/unit/commands-center.test.js +338 -0
- package/core/workspace-app/tests/unit/composer-menu.test.js +187 -0
- package/core/workspace-app/tests/unit/config-yaml.test.js +116 -0
- package/core/workspace-app/tests/unit/confirm-dialog.test.js +69 -0
- package/core/workspace-app/tests/unit/create-card.test.js +85 -0
- package/core/workspace-app/tests/unit/ddev-addons.test.js +144 -0
- package/core/workspace-app/tests/unit/ddev-auth-ssh.test.js +40 -0
- package/core/workspace-app/tests/unit/ddev-custom-commands.test.js +56 -0
- package/core/workspace-app/tests/unit/ddev-diagnose.test.js +57 -0
- package/core/workspace-app/tests/unit/ddev-domains.test.js +179 -0
- package/core/workspace-app/tests/unit/ddev-hostnames.test.js +143 -0
- package/core/workspace-app/tests/unit/ddev-import.test.js +82 -0
- package/core/workspace-app/tests/unit/ddev-mail.test.js +135 -0
- package/core/workspace-app/tests/unit/ddev-menu-and-create.test.js +28 -0
- package/core/workspace-app/tests/unit/ddev-mutagen.test.js +37 -0
- package/core/workspace-app/tests/unit/ddev-options.test.js +51 -0
- package/core/workspace-app/tests/unit/ddev-share.test.js +40 -0
- package/core/workspace-app/tests/unit/ddev-shim.test.js +57 -0
- package/core/workspace-app/tests/unit/ddev-snapshots.test.js +142 -0
- package/core/workspace-app/tests/unit/ddev-start-daemons.test.js +62 -0
- package/core/workspace-app/tests/unit/ddev-xdebug.test.js +41 -0
- package/core/workspace-app/tests/unit/deep-chat-origin.test.js +38 -0
- package/core/workspace-app/tests/unit/density.test.js +52 -0
- package/core/workspace-app/tests/unit/elevenlabs.test.js +453 -0
- package/core/workspace-app/tests/unit/event-log.test.js +102 -0
- package/core/workspace-app/tests/unit/favicon.test.js +68 -0
- package/core/workspace-app/tests/unit/files-browser.test.js +224 -0
- package/core/workspace-app/tests/unit/git-info.test.js +26 -0
- package/core/workspace-app/tests/unit/grid-and-list.test.js +103 -0
- package/core/workspace-app/tests/unit/host-tool-wrapper.test.js +18 -0
- package/core/workspace-app/tests/unit/html-patterns.test.js +51 -0
- package/core/workspace-app/tests/unit/ide-infra.test.js +28 -0
- package/core/workspace-app/tests/unit/images-ai.test.js +201 -0
- package/core/workspace-app/tests/unit/images-create.test.js +59 -0
- package/core/workspace-app/tests/unit/images-providers.test.js +248 -0
- package/core/workspace-app/tests/unit/images-svg.test.js +213 -0
- package/core/workspace-app/tests/unit/images-workspace.test.js +144 -0
- package/core/workspace-app/tests/unit/jobs-fragment.test.js +66 -0
- package/core/workspace-app/tests/unit/media-ux.test.js +164 -0
- package/core/workspace-app/tests/unit/modal-fit.test.js +87 -0
- package/core/workspace-app/tests/unit/modules-load.test.js +23 -0
- package/core/workspace-app/tests/unit/narration.test.js +186 -0
- package/core/workspace-app/tests/unit/navigation-editor.test.js +260 -0
- package/core/workspace-app/tests/unit/navigation.test.js +254 -0
- package/core/workspace-app/tests/unit/new-folder.test.js +121 -0
- package/core/workspace-app/tests/unit/new-screen-form.test.js +149 -0
- package/core/workspace-app/tests/unit/node-menu.test.js +161 -0
- package/core/workspace-app/tests/unit/notes.test.js +128 -0
- package/core/workspace-app/tests/unit/page-structure.test.js +40 -0
- package/core/workspace-app/tests/unit/page-width.test.js +59 -0
- package/core/workspace-app/tests/unit/php-version.test.js +90 -0
- package/core/workspace-app/tests/unit/plan-card.test.js +44 -0
- package/core/workspace-app/tests/unit/plugin-catalog.test.js +31 -0
- package/core/workspace-app/tests/unit/plugin-system.test.js +251 -0
- package/core/workspace-app/tests/unit/plugins-page.test.js +28 -0
- package/core/workspace-app/tests/unit/plugins.test.js +58 -0
- package/core/workspace-app/tests/unit/preconditions.test.js +21 -0
- package/core/workspace-app/tests/unit/project-activity-filter.test.js +180 -0
- package/core/workspace-app/tests/unit/project-make.test.js +143 -0
- package/core/workspace-app/tests/unit/project-name.test.js +202 -0
- package/core/workspace-app/tests/unit/reader.test.js +62 -0
- package/core/workspace-app/tests/unit/release.test.js +92 -0
- package/core/workspace-app/tests/unit/remove-confirm.test.js +37 -0
- package/core/workspace-app/tests/unit/report-pdf.test.js +171 -0
- package/core/workspace-app/tests/unit/round-11.test.js +31 -0
- package/core/workspace-app/tests/unit/round-12.test.js +37 -0
- package/core/workspace-app/tests/unit/row-layout.test.js +585 -0
- package/core/workspace-app/tests/unit/row-menus-fold.test.js +29 -0
- package/core/workspace-app/tests/unit/rows-add-modal.test.js +68 -0
- package/core/workspace-app/tests/unit/screen-runner-queue.test.js +41 -0
- package/core/workspace-app/tests/unit/screen-window.test.js +116 -0
- package/core/workspace-app/tests/unit/screens-beside-running.test.js +19 -0
- package/core/workspace-app/tests/unit/screens-columns.test.js +79 -0
- package/core/workspace-app/tests/unit/screens-menu.test.js +84 -0
- package/core/workspace-app/tests/unit/screens-modal-overlay.test.js +14 -0
- package/core/workspace-app/tests/unit/screens-waiting.test.js +129 -0
- package/core/workspace-app/tests/unit/screens.test.js +93 -0
- package/core/workspace-app/tests/unit/second-storage.test.js +218 -0
- package/core/workspace-app/tests/unit/session-name.test.js +64 -0
- package/core/workspace-app/tests/unit/settings-file.test.js +173 -0
- package/core/workspace-app/tests/unit/settings-nested-list.test.js +54 -0
- package/core/workspace-app/tests/unit/settings-raw.test.js +177 -0
- package/core/workspace-app/tests/unit/shared-prefs.test.js +49 -0
- package/core/workspace-app/tests/unit/speech.test.js +290 -0
- package/core/workspace-app/tests/unit/static-caching.test.js +39 -0
- package/core/workspace-app/tests/unit/status-language.test.js +47 -0
- package/core/workspace-app/tests/unit/tab-modal.test.js +69 -0
- package/core/workspace-app/tests/unit/tab-title.test.js +37 -0
- package/core/workspace-app/tests/unit/template-literals.test.js +52 -0
- package/core/workspace-app/tests/unit/terminal-scrollback.test.js +61 -0
- package/core/workspace-app/tests/unit/terminal-size.test.js +103 -0
- package/core/workspace-app/tests/unit/terminal-style.test.js +84 -0
- package/core/workspace-app/tests/unit/terminal-windows.test.js +154 -0
- package/core/workspace-app/tests/unit/themes.test.js +453 -0
- package/core/workspace-app/tests/unit/tool-labels.test.js +34 -0
- package/core/workspace-app/tests/unit/topbar-icons.test.js +47 -0
- package/core/workspace-app/tests/unit/update-docker-gid.test.js +51 -0
- package/core/workspace-app/tests/unit/user-guide.test.js +60 -0
- package/core/workspace-app/tests/unit/version-check.test.js +98 -0
- package/core/workspace-app/tests/unit/version-link.test.js +50 -0
- package/core/workspace-app/tests/unit/video-borrow.test.js +220 -0
- package/core/workspace-app/tests/unit/video-edit-ask.test.js +93 -0
- package/core/workspace-app/tests/unit/video-edit-short.test.js +42 -0
- package/core/workspace-app/tests/unit/video-edit.test.js +436 -0
- package/core/workspace-app/tests/unit/video-transcript.test.js +237 -0
- package/core/workspace-app/tests/unit/waiting.test.js +48 -0
- package/core/workspace-app/tests/unit/wordpress-odoo-builders.test.js +38 -0
- package/core/workspace-app/tests/unit/workspace-bin.test.js +48 -0
- package/core/workspace-app/tests/unit/workspace-card-tools.test.js +30 -0
- package/core/workspace-app/tests/unit/workspace-delete.test.js +77 -0
- package/core/workspace-app/tests/unit/workspace-types.test.js +68 -0
- package/core/workspace-app/tests/unit/worktree-clone.test.js +250 -0
- package/package.json +38 -36
- /package/{WORKSPACES.md → core/workspace-app/WORKSPACES.md} +0 -0
- /package/{actions → core/workspace-app/actions}/ai-items.js +0 -0
- /package/{actions → core/workspace-app/actions}/audio.js +0 -0
- /package/{actions → core/workspace-app/actions}/build-order.js +0 -0
- /package/{actions → core/workspace-app/actions}/checks.js +0 -0
- /package/{actions → core/workspace-app/actions}/claude-session.js +0 -0
- /package/{actions → core/workspace-app/actions}/commands-center.js +0 -0
- /package/{actions → core/workspace-app/actions}/composer.js +0 -0
- /package/{actions → core/workspace-app/actions}/ddev-addons.js +0 -0
- /package/{actions → core/workspace-app/actions}/ddev-mail.js +0 -0
- /package/{actions → core/workspace-app/actions}/folders.js +0 -0
- /package/{actions → core/workspace-app/actions}/images-svg.js +0 -0
- /package/{actions → core/workspace-app/actions}/images.js +0 -0
- /package/{actions → core/workspace-app/actions}/navigation.js +0 -0
- /package/{actions → core/workspace-app/actions}/node.js +0 -0
- /package/{actions → core/workspace-app/actions}/notes.js +0 -0
- /package/{actions → core/workspace-app/actions}/plugins.js +0 -0
- /package/{actions → core/workspace-app/actions}/project-make.js +0 -0
- /package/{actions → core/workspace-app/actions}/row-actions.js +0 -0
- /package/{actions → core/workspace-app/actions}/screens.js +0 -0
- /package/{actions → core/workspace-app/actions}/video-edit.js +0 -0
- /package/{ai-inventory.js → core/workspace-app/ai-inventory.js} +0 -0
- /package/{ai-items.js → core/workspace-app/ai-items.js} +0 -0
- /package/{assistant-cast.js → core/workspace-app/assistant-cast.js} +0 -0
- /package/{assistant-suggestions.js → core/workspace-app/assistant-suggestions.js} +0 -0
- /package/{audio-voice.js → core/workspace-app/audio-voice.js} +0 -0
- /package/{auth-google.js → core/workspace-app/auth-google.js} +0 -0
- /package/{build-ai.js → core/workspace-app/build-ai.js} +0 -0
- /package/{build-order-page.js → core/workspace-app/build-order-page.js} +0 -0
- /package/{build-order.js → core/workspace-app/build-order.js} +0 -0
- /package/{builder-args.js → core/workspace-app/builder-args.js} +0 -0
- /package/{changelog.js → core/workspace-app/changelog.js} +0 -0
- /package/{chat-threads.js → core/workspace-app/chat-threads.js} +0 -0
- /package/{checks.js → core/workspace-app/checks.js} +0 -0
- /package/{claude-args.js → core/workspace-app/claude-args.js} +0 -0
- /package/{claude-sessions.js → core/workspace-app/claude-sessions.js} +0 -0
- /package/{command-author.js → core/workspace-app/command-author.js} +0 -0
- /package/{command-block.js → core/workspace-app/command-block.js} +0 -0
- /package/{commands → core/workspace-app/commands}/defaults.yml +0 -0
- /package/{commands-page.js → core/workspace-app/commands-page.js} +0 -0
- /package/{commands.js → core/workspace-app/commands.js} +0 -0
- /package/{cucumber.js → core/workspace-app/cucumber.js} +0 -0
- /package/{ddev-addons-page.js → core/workspace-app/ddev-addons-page.js} +0 -0
- /package/{ddev-addons.js → core/workspace-app/ddev-addons.js} +0 -0
- /package/{ddev-custom-commands.js → core/workspace-app/ddev-custom-commands.js} +0 -0
- /package/{ddev-domains.js → core/workspace-app/ddev-domains.js} +0 -0
- /package/{ddev-import.js → core/workspace-app/ddev-import.js} +0 -0
- /package/{ddev-mail-page.js → core/workspace-app/ddev-mail-page.js} +0 -0
- /package/{ddev-mail.js → core/workspace-app/ddev-mail.js} +0 -0
- /package/{ddev-options.js → core/workspace-app/ddev-options.js} +0 -0
- /package/{ddev-snapshots.js → core/workspace-app/ddev-snapshots.js} +0 -0
- /package/{ddev.js → core/workspace-app/ddev.js} +0 -0
- /package/{docs → core/workspace-app/docs}/DEVELOPER-GUIDE.md +0 -0
- /package/{docs → core/workspace-app/docs}/README.md +0 -0
- /package/{docs → core/workspace-app/docs}/REFERENCE.md +0 -0
- /package/{docs → core/workspace-app/docs}/USER-GUIDE.md +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-board.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-coffee.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-colours.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-icons.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-moves.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-pick-cookieman.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-settings-voices.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-status.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/create-card/create-card.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/create-card/start-ai.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-address.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-off.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-on.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-ddev-menu.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-domains.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-home.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-ide-menu.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-remove-refusal.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-rows-worktree.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-screens.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-settings.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-workspace-page.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/home/card-settings.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/home/drag-reorder.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/home/edit-card.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/claude-code-image.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/edit-with-ai.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/gallery.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/generate-description.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/generate-plan.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/providers.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/svg-form.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/svg-result.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/upload.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/new-folder/2-form.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/new-folder/8-ddev-options.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/new-folder/files-folder.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/projects/ddev-custom-commands.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/projects/state-chip-stop.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/ai.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/code.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/docs.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/services.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/sites.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-scenarios.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-steps.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/screens-menu.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/screens-new.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/auth-google-lists.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-1-row.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-2-confirm.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-3-type-name.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-4-type-yes.png +0 -0
- /package/{event-log.js → core/workspace-app/event-log.js} +0 -0
- /package/{files-browser.js → core/workspace-app/files-browser.js} +0 -0
- /package/{folders.js → core/workspace-app/folders.js} +0 -0
- /package/{format.js → core/workspace-app/format.js} +0 -0
- /package/{github-auth.js → core/workspace-app/github-auth.js} +0 -0
- /package/{html.js → core/workspace-app/html.js} +0 -0
- /package/{icons.js → core/workspace-app/icons.js} +0 -0
- /package/{images-ai.js → core/workspace-app/images-ai.js} +0 -0
- /package/{images-create.js → core/workspace-app/images-create.js} +0 -0
- /package/{images-page.js → core/workspace-app/images-page.js} +0 -0
- /package/{images-records.js → core/workspace-app/images-records.js} +0 -0
- /package/{images-render.js → core/workspace-app/images-render.js} +0 -0
- /package/{images-svg-core.js → core/workspace-app/images-svg-core.js} +0 -0
- /package/{images-svg-sanitize.js → core/workspace-app/images-svg-sanitize.js} +0 -0
- /package/{images-svg.js → core/workspace-app/images-svg.js} +0 -0
- /package/{images.js → core/workspace-app/images.js} +0 -0
- /package/{jobs.js → core/workspace-app/jobs.js} +0 -0
- /package/{linger.js → core/workspace-app/linger.js} +0 -0
- /package/{lists.js → core/workspace-app/lists.js} +0 -0
- /package/{media-info.js → core/workspace-app/media-info.js} +0 -0
- /package/{navigation-page.js → core/workspace-app/navigation-page.js} +0 -0
- /package/{navigation.js → core/workspace-app/navigation.js} +0 -0
- /package/{notes.js → core/workspace-app/notes.js} +0 -0
- /package/{palette-index.js → core/workspace-app/palette-index.js} +0 -0
- /package/{playwright.config.js → core/workspace-app/playwright.config.js} +0 -0
- /package/{plugin-install.js → core/workspace-app/plugin-install.js} +0 -0
- /package/{plugin-loader.js → core/workspace-app/plugin-loader.js} +0 -0
- /package/{plugin-manifest.js → core/workspace-app/plugin-manifest.js} +0 -0
- /package/{plugin-settings.js → core/workspace-app/plugin-settings.js} +0 -0
- /package/{plugins → core/workspace-app/plugins}/workspace-notes/index.js +0 -0
- /package/{project-make.js → core/workspace-app/project-make.js} +0 -0
- /package/{project-name.js → core/workspace-app/project-name.js} +0 -0
- /package/{public → core/workspace-app/public}/assistant-avatar-moves.js +0 -0
- /package/{public → core/workspace-app/public}/assistant-avatar-ui.js +0 -0
- /package/{public → core/workspace-app/public}/assistant-avatar.js +0 -0
- /package/{public → core/workspace-app/public}/avatar/druplicon.vrm +0 -0
- /package/{public → core/workspace-app/public}/css/base.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/ai-assistant.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/assistant-avatar.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/assistant-views.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/build-order.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/clone-as-a-git-worktree.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/command-palette.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/commands-center.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/confirm-dialog.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/create-card.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/ddev-menu.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/deep-chat.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/density.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/floating-assistant-workspace-pages.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/folders.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/full-height-left-assistant-sidebar-all-pages.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/full-height-right-actions-rail-pages-that-have-actions.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/grid-and-list.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/help-page.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/hero.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/images.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/layout-rhythm-and-spacing-polish.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/mic-on-any-field-that-wants-typing.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/modal-fit.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/page-width.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/plugins-page.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/project-cards.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/project-rows.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/reordering-the-workspace-cards.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/responsive-home-and-the-workspace-pages.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/running-commands-as-toasts.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/settings-editor-core-config-yml.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/short-results-as-a-toast-at-the-top.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/sound-player.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/suggest-agents-skills-commands.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/tab-modal.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/terminals.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/test-report-media-one-file-per-row-paged.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/top-toolbar-logo-breadcrumb.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/video-edit.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/web-ide-open-with.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/working-indicators.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/workspace-cards.css +0 -0
- /package/{public → core/workspace-app/public}/favicon.ico +0 -0
- /package/{public → core/workspace-app/public}/favicon.png +0 -0
- /package/{public → core/workspace-app/public}/htmx.min.js +0 -0
- /package/{public → core/workspace-app/public}/logo.png +0 -0
- /package/{public → core/workspace-app/public}/media-player.js +0 -0
- /package/{public → core/workspace-app/public}/mic.js +0 -0
- /package/{public → core/workspace-app/public}/narration.js +0 -0
- /package/{public → core/workspace-app/public}/palette.js +0 -0
- /package/{public → core/workspace-app/public}/sortable.js +0 -0
- /package/{public → core/workspace-app/public}/suggest.js +0 -0
- /package/{public → core/workspace-app/public}/terminal.js +0 -0
- /package/{public → core/workspace-app/public}/themes/default/theme.css +0 -0
- /package/{public → core/workspace-app/public}/themes/ember/theme.css +0 -0
- /package/{public → core/workspace-app/public}/themes/press/theme.css +0 -0
- /package/{public → core/workspace-app/public}/themes/slate/theme.css +0 -0
- /package/{public → core/workspace-app/public}/touch-icon.png +0 -0
- /package/{public → core/workspace-app/public}/ui.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/ace.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/ext-language_tools.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/ext-searchbox.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-css.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-html.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-javascript.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-markdown.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-sh.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-yaml.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/theme-textmate.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/theme-tomorrow_night.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/worker-yaml.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/deep-chat.LICENSE +0 -0
- /package/{public → core/workspace-app/public}/vendor/deep-chat.bundle.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/tabler-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/tabler-sprite.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/LICENSE +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/addons/loaders/GLTFLoader.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/addons/utils/BufferGeometryUtils.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/addons/utils/SkeletonUtils.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/three.core.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/three.module.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three-vrm/LICENSE +0 -0
- /package/{public → core/workspace-app/public}/vendor/three-vrm/three-vrm.module.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/uikit-icons.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/uikit.min.css +0 -0
- /package/{public → core/workspace-app/public}/vendor/uikit.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/bookmark-outlined.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/headset.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/menu.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/pause.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/play.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/share.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-off.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-on.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/stop.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/addon-fit.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/addon-unicode11.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/addon-web-links.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/xterm.css +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/xterm.js +0 -0
- /package/{rag.js → core/workspace-app/rag.js} +0 -0
- /package/{reader.js → core/workspace-app/reader.js} +0 -0
- /package/{repo-blobs.js → core/workspace-app/repo-blobs.js} +0 -0
- /package/{report-pdf.js → core/workspace-app/report-pdf.js} +0 -0
- /package/{row-layout.js → core/workspace-app/row-layout.js} +0 -0
- /package/{screenrc → core/workspace-app/screenrc} +0 -0
- /package/{screens.js → core/workspace-app/screens.js} +0 -0
- /package/{scripts → core/workspace-app/scripts}/code-server-start.sh +0 -0
- /package/{scripts → core/workspace-app/scripts}/images-claude-finish.js +0 -0
- /package/{scripts → core/workspace-app/scripts}/screen-host.sh +0 -0
- /package/{scripts → core/workspace-app/scripts}/screen-runner.js +0 -0
- /package/{scripts → core/workspace-app/scripts}/screen-then.js +0 -0
- /package/{scripts → core/workspace-app/scripts}/update.sh +0 -0
- /package/{settings-file.js → core/workspace-app/settings-file.js} +0 -0
- /package/{speech.js → core/workspace-app/speech.js} +0 -0
- /package/{tab-modal.js → core/workspace-app/tab-modal.js} +0 -0
- /package/{test-reports.js → core/workspace-app/test-reports.js} +0 -0
- /package/{themes.js → core/workspace-app/themes.js} +0 -0
- /package/{transcripts.js → core/workspace-app/transcripts.js} +0 -0
- /package/{urls.js → core/workspace-app/urls.js} +0 -0
- /package/{validate.js → core/workspace-app/validate.js} +0 -0
- /package/{version-check.js → core/workspace-app/version-check.js} +0 -0
- /package/{video-edit.js → core/workspace-app/video-edit.js} +0 -0
- /package/{waiting.js → core/workspace-app/waiting.js} +0 -0
- /package/{workspace-card-tools.js → core/workspace-app/workspace-card-tools.js} +0 -0
- /package/{workspace-delete.js → core/workspace-app/workspace-delete.js} +0 -0
- /package/{workspace-types.js → core/workspace-app/workspace-types.js} +0 -0
- /package/{workspaces.js → core/workspace-app/workspaces.js} +0 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a voice: the engines the form offers follow the keys the machine holds.
|
|
3
|
+
*
|
|
4
|
+
* The real server and the real `audio/cmd-tools-voice.sh` run here, against a stub of the ElevenLabs
|
|
5
|
+
* API and a home with a fake voice-narration catalog, so nothing is spent and nothing depends on what
|
|
6
|
+
* is installed on the machine running the tests.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { test, before, after } = require('node:test');
|
|
10
|
+
const assert = require('node:assert');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const http = require('http');
|
|
13
|
+
const net = require('net');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { spawn, execFile } = require('child_process');
|
|
17
|
+
const { promisify } = require('util');
|
|
18
|
+
const run = promisify(execFile);
|
|
19
|
+
|
|
20
|
+
const APP = path.join(__dirname, '../..');
|
|
21
|
+
const REPO = path.join(APP, '../..');
|
|
22
|
+
const { copyConfig } = require('./_isolated-config');
|
|
23
|
+
|
|
24
|
+
// The workspace root is read when the app's modules first load, so it is made and named before they do.
|
|
25
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-voice-root-'));
|
|
26
|
+
process.env.WORKSPACE_ROOT = root;
|
|
27
|
+
const voice = require('../../audio-voice');
|
|
28
|
+
|
|
29
|
+
const SAYS = `#!/usr/bin/env bash
|
|
30
|
+
echo "claude-say voices (use: claude-say -v <number> or -v <basename>):"
|
|
31
|
+
echo " 1) en_US-lessac-high American English — high (default)"
|
|
32
|
+
echo " !2) en_GB-cori-high British English — female, high"
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const TIMED = `# Demo
|
|
36
|
+
|
|
37
|
+
## S01 Opening (00:00.0 to 00:10.0)
|
|
38
|
+
|
|
39
|
+
> This is the opening.
|
|
40
|
+
> *(optional)* It has a spare sentence.
|
|
41
|
+
|
|
42
|
+
## S02 Close (00:10.0 to 00:20.0)
|
|
43
|
+
|
|
44
|
+
> Thank you for watching.
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const calls = [];
|
|
48
|
+
let home; let stub; let stubBase; let voicesAllowed = true; let modelsAllowed = false; const servers = [];
|
|
49
|
+
|
|
50
|
+
const free = () => new Promise((resolve) => {
|
|
51
|
+
const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
before(async () => {
|
|
55
|
+
home = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-voice-home-'));
|
|
56
|
+
// ffmpeg and ffprobe are made visible and nothing else from the machine's own PATH: Piper must stay hidden.
|
|
57
|
+
fs.mkdirSync(path.join(home, 'bin'), { recursive: true });
|
|
58
|
+
for (const tool of ['ffmpeg', 'ffprobe']) {
|
|
59
|
+
const found = (process.env.PATH || '').split(path.delimiter).map((d) => path.join(d, tool)).find((f) => fs.existsSync(f));
|
|
60
|
+
if (found) fs.symlinkSync(found, path.join(home, 'bin', tool));
|
|
61
|
+
}
|
|
62
|
+
fs.mkdirSync(path.join(home, '.claude/skills/voice-narration/scripts'), { recursive: true });
|
|
63
|
+
fs.writeFileSync(path.join(home, '.claude/skills/voice-narration/scripts/claude-say'), SAYS);
|
|
64
|
+
fs.mkdirSync(path.join(root, 'core'), { recursive: true });
|
|
65
|
+
copyConfig(path.join(REPO, 'core/config'), path.join(root, 'core/config'));
|
|
66
|
+
fs.symlinkSync(path.join(REPO, 'core/scripts'), path.join(root, 'core/scripts'));
|
|
67
|
+
fs.symlinkSync(APP, path.join(root, 'core/workspace-app'));
|
|
68
|
+
fs.mkdirSync(path.join(root, 'audio'), { recursive: true });
|
|
69
|
+
fs.mkdirSync(path.join(root, 'videos'), { recursive: true });
|
|
70
|
+
fs.copyFileSync(path.join(REPO, 'videos/cmd-tools-elevenlabs.sh'), path.join(root, 'videos/cmd-tools-elevenlabs.sh'));
|
|
71
|
+
fs.mkdirSync(path.join(root, 'docs'), { recursive: true });
|
|
72
|
+
fs.writeFileSync(path.join(root, 'videos/demo.mp4'), Buffer.alloc(100, 1));
|
|
73
|
+
fs.writeFileSync(path.join(root, 'docs/demo-voiceover-script.md'), TIMED);
|
|
74
|
+
fs.writeFileSync(path.join(root, 'docs/notes.md'), 'not a script');
|
|
75
|
+
fs.copyFileSync(path.join(REPO, 'audio/cmd-tools-voice.sh'), path.join(root, 'audio/cmd-tools-voice.sh'));
|
|
76
|
+
stub = http.createServer((req, res) => {
|
|
77
|
+
const json = (code, body) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(body)); };
|
|
78
|
+
if (req.headers['xi-api-key'] !== 'testkey') return json(401, { detail: { message: 'bad key' } });
|
|
79
|
+
if (req.method === 'POST' && /^\/v1\/(sound-generation|music|text-to-speech\/)/.test(req.url)) {
|
|
80
|
+
let raw = '';
|
|
81
|
+
req.on('data', (c) => { raw += c; });
|
|
82
|
+
req.on('end', () => { calls.push({ url: req.url, body: JSON.parse(raw || '{}') }); res.writeHead(200, { 'Content-Type': 'audio/mpeg' }); res.end(Buffer.alloc(900, 7)); });
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
if (req.url.startsWith('/v1/user/subscription')) return json(200, { tier: 'creator', status: 'active', character_count: 100, character_limit: 1100, next_character_count_reset_unix: 1800000000 });
|
|
86
|
+
if (req.url.startsWith('/v1/models')) {
|
|
87
|
+
if (!modelsAllowed) return json(401, { detail: { message: 'missing the permission models_read' } });
|
|
88
|
+
return json(200, [{ model_id: 'eleven_flash_v2_5', name: 'Flash', can_do_text_to_speech: true },
|
|
89
|
+
{ model_id: 'eleven_new_v9', name: 'New v9', can_do_text_to_speech: true }, { model_id: 'eleven_stt', name: 'Speech to text', can_do_text_to_speech: false }]);
|
|
90
|
+
}
|
|
91
|
+
if (req.url.startsWith('/v2/voices')) {
|
|
92
|
+
if (!voicesAllowed) return json(403, { detail: { message: 'missing the permission voices_read' } });
|
|
93
|
+
return json(200, { voices: [{ voice_id: 'voiceAAAAAAAAAAAAAAAA', name: 'Alice', category: 'premade', labels: { gender: 'female', accent: 'british' } },
|
|
94
|
+
{ voice_id: 'voiceBBBBBBBBBBBBBBBB', name: 'Bob', category: 'premade', labels: {} }] });
|
|
95
|
+
}
|
|
96
|
+
return json(404, {});
|
|
97
|
+
});
|
|
98
|
+
await new Promise((r) => stub.listen(0, '127.0.0.1', r));
|
|
99
|
+
stubBase = `http://127.0.0.1:${stub.address().port}`;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
after(() => {
|
|
103
|
+
for (const s of servers) s.kill();
|
|
104
|
+
stub.close();
|
|
105
|
+
for (const d of [root, home]) fs.rmSync(d, { recursive: true, force: true });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const baseEnv = (extra = {}) => ({ PATH: `/usr/bin:/bin:${path.dirname(process.execPath)}:${path.join(home, 'bin')}`, HOME: home, WORKSPACE_ROOT: root, ELEVENLABS_API_BASE: stubBase, ...extra });
|
|
109
|
+
|
|
110
|
+
// Asynchronous on purpose: the stub API answers from this process, which a synchronous call would block.
|
|
111
|
+
async function engines(extra) {
|
|
112
|
+
const { stdout } = await run('bash', [path.join(root, 'audio/cmd-tools-voice.sh'), '-a', 'engines', '--json'], { env: baseEnv(extra) });
|
|
113
|
+
return JSON.parse(stdout).engines;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function serve(extra) {
|
|
117
|
+
const port = await free();
|
|
118
|
+
const proc = spawn(process.execPath, ['server.js'], { cwd: APP, env: { ...baseEnv(extra), PORT: String(port) }, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
119
|
+
servers.push(proc);
|
|
120
|
+
await new Promise((resolve, reject) => {
|
|
121
|
+
const t = setTimeout(() => reject(new Error('server did not start')), 15000);
|
|
122
|
+
proc.stdout.on('data', (d) => { if (/listening/.test(String(d))) { clearTimeout(t); resolve(); } });
|
|
123
|
+
proc.on('exit', (c) => { clearTimeout(t); reject(new Error(`server exited ${c}`)); });
|
|
124
|
+
});
|
|
125
|
+
return `http://127.0.0.1:${port}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const post = (base, p, form) => fetch(base + p, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(form) });
|
|
129
|
+
|
|
130
|
+
test('with a key the command offers ElevenLabs, its voices and its credits; without one it says why not', async () => {
|
|
131
|
+
voicesAllowed = true;
|
|
132
|
+
const [piper, eleven] = await engines({ ELEVENLABS_API_KEY: 'testkey' });
|
|
133
|
+
assert.strictEqual(eleven.available, true);
|
|
134
|
+
assert.strictEqual(eleven.can_list_voices, true);
|
|
135
|
+
assert.deepStrictEqual(eleven.voices.map((v) => v.id), ['voiceAAAAAAAAAAAAAAAA', 'voiceBBBBBBBBBBBBBBBB']);
|
|
136
|
+
assert.strictEqual(eleven.remaining, 1000);
|
|
137
|
+
assert.strictEqual(eleven.plan, 'creator');
|
|
138
|
+
assert.ok(eleven.models.some((m) => m.id === 'eleven_v4'));
|
|
139
|
+
assert.strictEqual(piper.reason_code, 'no-binary');
|
|
140
|
+
assert.deepStrictEqual(piper.voices.map((v) => [v.id, v.number]), [['en_US-lessac-high', 1], ['en_GB-cori-high', 2]]);
|
|
141
|
+
|
|
142
|
+
const bare = (await engines({ ELEVENLABS_API_KEY: '' }))[1];
|
|
143
|
+
assert.strictEqual(bare.available, false);
|
|
144
|
+
assert.match(bare.reason, /No ElevenLabs key/);
|
|
145
|
+
const bad = (await engines({ ELEVENLABS_API_KEY: 'wrongkey' }))[1];
|
|
146
|
+
assert.strictEqual(bad.available, false);
|
|
147
|
+
assert.match(bad.reason, /rejected the key/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('a voice given by name is resolved to its id, and the settings accept voice and model', async () => {
|
|
151
|
+
const eleven = (await engines({ ELEVENLABS_API_KEY: 'testkey', ELEVENLABS_VOICE: 'alice' }))[1];
|
|
152
|
+
assert.strictEqual(eleven.default_voice, 'voiceAAAAAAAAAAAAAAAA');
|
|
153
|
+
const text = path.join(home, 'name.txt');
|
|
154
|
+
fs.writeFileSync(text, 'Hello');
|
|
155
|
+
const cli = path.join(root, 'audio/cmd-tools-voice.sh');
|
|
156
|
+
const plan = await run('bash', [cli, '-a', 'speak', '--engine', 'elevenlabs', '--text-file', text, '--out', path.join(home, 'n.mp3'), '--voice', 'Alice'], { env: baseEnv({ ELEVENLABS_API_KEY: 'testkey' }) }).catch((e) => e);
|
|
157
|
+
assert.strictEqual(plan.code, 4);
|
|
158
|
+
assert.match(plan.stdout, /Voice voiceAAAAAAAAAAAAAAAA,/);
|
|
159
|
+
const unknown = await run('bash', [cli, '-a', 'speak', '--engine', 'elevenlabs', '--text-file', text, '--out', path.join(home, 'n.mp3'), '--voice', 'Nobody'], { env: baseEnv({ ELEVENLABS_API_KEY: 'testkey' }) }).catch((e) => e);
|
|
160
|
+
assert.match(unknown.stderr, /No voice named "Nobody"/);
|
|
161
|
+
// The shorter key names some settings files use.
|
|
162
|
+
const file = path.join(root, 'core/config/settings.yml');
|
|
163
|
+
const original = fs.readFileSync(file, 'utf8');
|
|
164
|
+
fs.writeFileSync(file, original.replace(/^ {2}voice_id: .*\n/m, ' voice: Alice\n').replace(/^ {2}model_id: .*\n/m, ' model: eleven_v3\n'));
|
|
165
|
+
try {
|
|
166
|
+
const status = (await run('bash', [path.join(root, 'videos/cmd-tools-elevenlabs.sh'), '-a', 'status'], { env: baseEnv({ ELEVENLABS_API_KEY: 'testkey' }) })).stdout;
|
|
167
|
+
assert.match(status, /model: {3}eleven_v3/);
|
|
168
|
+
assert.match(status, /voice: {3}Alice/);
|
|
169
|
+
} finally { fs.writeFileSync(file, original); }
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('a key that cannot list voices is still offered, and asks for a voice id', async () => {
|
|
173
|
+
voicesAllowed = false;
|
|
174
|
+
const eleven = (await engines({ ELEVENLABS_API_KEY: 'testkey' }))[1];
|
|
175
|
+
voicesAllowed = true;
|
|
176
|
+
assert.strictEqual(eleven.available, true);
|
|
177
|
+
assert.strictEqual(eleven.can_list_voices, false);
|
|
178
|
+
assert.strictEqual(eleven.can_read_account, true);
|
|
179
|
+
assert.match(eleven.voices_note, /voices_read/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('Piper is offered from the host catalog even where its binary cannot be seen', async () => {
|
|
183
|
+
const found = voice.offered(await engines({ ELEVENLABS_API_KEY: 'testkey' }));
|
|
184
|
+
const piper = found.find((e) => e.id === 'piper');
|
|
185
|
+
assert.strictEqual(piper.available, true);
|
|
186
|
+
assert.strictEqual(piper.hostOnly, true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('a request is checked against the engines on offer before anything is written or run', async () => {
|
|
190
|
+
const found = await engines({ ELEVENLABS_API_KEY: 'testkey' });
|
|
191
|
+
const good = { text: 'Hello there', engine: 'elevenlabs', el_voice: 'voiceAAAAAAAAAAAAAAAA', el_model: 'eleven_v4' };
|
|
192
|
+
const p = voice.plan(good, found);
|
|
193
|
+
assert.deepStrictEqual([p.engine, p.voice, p.model, p.ext, p.characters], ['elevenlabs', 'voiceAAAAAAAAAAAAAAAA', 'eleven_v4', 'mp3', 11]);
|
|
194
|
+
assert.match(p.file, /^hello-there-elevenlabs\.mp3$/);
|
|
195
|
+
const err = (form) => voice.plan({ ...good, ...form }, found).error;
|
|
196
|
+
assert.match(err({ text: ' ' }), /Type what/);
|
|
197
|
+
assert.match(err({ text: 'x'.repeat(5001) }), /at most 5000/);
|
|
198
|
+
assert.match(err({ engine: 'other' }), /Choose a voice/);
|
|
199
|
+
assert.match(err({ el_voice: 'nobody' }), /not on this account/);
|
|
200
|
+
assert.match(err({ el_voice: 'a b; rm' }), /Choose a voice/);
|
|
201
|
+
assert.match(err({ el_model: 'eleven_nope' }), /Choose one of the models/);
|
|
202
|
+
assert.match(voice.plan({ text: 'Hi', engine: 'piper', piper_voice: 'nope' }, found).error, /Piper voices/);
|
|
203
|
+
const piper = voice.plan({ text: 'Hi', engine: 'piper', piper_voice: 'en_US-lessac-high', piper_format: 'mp3', name: 'My Greeting!' }, found);
|
|
204
|
+
assert.deepStrictEqual([piper.ext, piper.file], ['mp3', 'my-greeting.mp3']);
|
|
205
|
+
const noKey = await engines({ ELEVENLABS_API_KEY: '' });
|
|
206
|
+
assert.match(voice.plan(good, noKey).error, /not available/);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('the line the host runs holds no shell expansion, and only ElevenLabs carries --confirm', async () => {
|
|
210
|
+
const found = await engines({ ELEVENLABS_API_KEY: 'testkey' });
|
|
211
|
+
const eleven = voice.commandLine(voice.plan({ text: 'Hi', engine: 'elevenlabs', el_voice: 'voiceAAAAAAAAAAAAAAAA' }, found), '/tmp/ws-voice-x/text.txt', true);
|
|
212
|
+
const piper = voice.commandLine(voice.plan({ text: 'Hi', engine: 'piper', piper_voice: 'en_US-lessac-high' }, found), '/tmp/ws-voice-x/text.txt', true);
|
|
213
|
+
for (const line of [eleven, piper]) {
|
|
214
|
+
assert.ok(!/[$`]/.test(line), line);
|
|
215
|
+
assert.match(line, /rm -rf "\/tmp\/ws-voice-x"/);
|
|
216
|
+
}
|
|
217
|
+
assert.match(eleven, /"--confirm"/);
|
|
218
|
+
assert.match(eleven, /"--model" "eleven_v4"/);
|
|
219
|
+
assert.ok(!/--confirm/.test(piper));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('the form offers what the keys allow, and the spending button asks first', async () => {
|
|
223
|
+
const base = await serve({ ELEVENLABS_API_KEY: 'testkey' });
|
|
224
|
+
const html = await (await fetch(`${base}/fragments/audio/new`)).text();
|
|
225
|
+
assert.match(html, /Generate a voice/);
|
|
226
|
+
assert.match(html, /<select class="uk-select" id="voice_choice" name="voice_choice">/, 'one dropdown for every voice');
|
|
227
|
+
assert.match(html, /<optgroup label="ElevenLabs — uses credits · 1,000 left on the creator plan">[\s\S]*<optgroup label="Piper — free, on this machine">/, 'ElevenLabs first, then Piper');
|
|
228
|
+
assert.match(html, /<option value="elevenlabs:voiceAAAAAAAAAAAAAAAA">Alice — female, british<\/option>/);
|
|
229
|
+
assert.match(html, /<option value="elevenlabs:voiceBBBBBBBBBBBBBBBB">Bob<\/option>/);
|
|
230
|
+
assert.match(html, /<option value="piper:en_US-lessac-high" selected>/, 'with no default voice set, the first Piper voice');
|
|
231
|
+
assert.ok(!/name="engine"/.test(html), 'the engine follows the voice, so there is no separate choice');
|
|
232
|
+
assert.match(html, /<option value="eleven_v4" selected>/);
|
|
233
|
+
assert.match(html, /host runner/);
|
|
234
|
+
assert.match(html, /arm-step" data-engine-button="elevenlabs" data-mode-button="text" hidden/);
|
|
235
|
+
assert.match(html, /hx-vals='\{"confirm":"yes"\}'[\s\S]*hx-trigger="confirmed-remove"/);
|
|
236
|
+
assert.match(html, /data-engine-pane="elevenlabs" hidden/);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('without a key the ElevenLabs option is disabled with its reason, and Piper still works', async () => {
|
|
240
|
+
const base = await serve({ ELEVENLABS_API_KEY: '' });
|
|
241
|
+
const html = await (await fetch(`${base}/fragments/audio/new`)).text();
|
|
242
|
+
assert.match(html, /<strong>ElevenLabs<\/strong> is not available: No ElevenLabs key is set/);
|
|
243
|
+
assert.ok(!/<optgroup label="ElevenLabs/.test(html), 'no ElevenLabs voices to pick');
|
|
244
|
+
assert.ok(!/data-engine-button="elevenlabs"/.test(html));
|
|
245
|
+
assert.match(html, /data-engine-button="piper"/);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('more models are offered, any model id can be typed, and a key that may read models adds its own', async () => {
|
|
249
|
+
modelsAllowed = false;
|
|
250
|
+
const [, eleven] = await engines({ ELEVENLABS_API_KEY: 'testkey' });
|
|
251
|
+
const ids = eleven.models.map((m) => m.id);
|
|
252
|
+
for (const id of ['eleven_v4', 'eleven_v3', 'eleven_multilingual_v2', 'eleven_flash_v2_5', 'eleven_turbo_v2_5', 'eleven_flash_v2', 'eleven_turbo_v2']) assert.ok(ids.includes(id), id);
|
|
253
|
+
assert.ok(!eleven.can_list_models && !ids.includes('eleven_new_v9'), 'without models_read, the built-in list');
|
|
254
|
+
assert.match(eleven.models.find((m) => m.id === 'eleven_flash_v2_5').label, /half the credits/, 'a label says what it costs');
|
|
255
|
+
modelsAllowed = true;
|
|
256
|
+
const [, listed] = await engines({ ELEVENLABS_API_KEY: 'testkey' });
|
|
257
|
+
modelsAllowed = false;
|
|
258
|
+
assert.strictEqual(listed.can_list_models, true);
|
|
259
|
+
assert.ok(listed.models.some((m) => m.id === 'eleven_new_v9' && m.label === 'New v9'), 'a speech model the key lists is added');
|
|
260
|
+
assert.ok(!listed.models.some((m) => m.id === 'eleven_stt'), 'one that cannot speak is not');
|
|
261
|
+
assert.strictEqual(listed.models.filter((m) => m.id === 'eleven_flash_v2_5').length, 1, 'and nothing is listed twice');
|
|
262
|
+
const found = [{ ...(await engines({ ELEVENLABS_API_KEY: 'testkey' }))[0] }, eleven];
|
|
263
|
+
const plan = (extra) => voice.plan({ text: 'Hi', voice_choice: 'elevenlabs:voiceAAAAAAAAAAAAAAAA', ...extra }, found);
|
|
264
|
+
assert.strictEqual(plan({ el_model: 'eleven_flash_v2_5' }).model, 'eleven_flash_v2_5');
|
|
265
|
+
assert.strictEqual(plan({ el_model: 'eleven_v3', el_model_other: 'eleven_brand_new' }).model, 'eleven_brand_new', 'a typed model wins over the list');
|
|
266
|
+
assert.match(plan({ el_model_other: 'bad model; rm' }).error, /not a model id/);
|
|
267
|
+
assert.match(plan({ el_model: 'eleven_nope' }).error, /Choose one of the models/, 'the list itself stays closed');
|
|
268
|
+
const html = voice.formHtml({ engines: found }, 'voice');
|
|
269
|
+
assert.match(html, /<option value="eleven_flash_v2_5">Flash v2\.5: fastest/);
|
|
270
|
+
assert.match(html, /name="el_model_other"/);
|
|
271
|
+
assert.match(voice.commandLine(plan({ el_model_other: 'eleven_brand_new' }), '/tmp/ws-voice-x/text.txt', true), /"--model" "eleven_brand_new"/);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('the configured default voice is the one selected, and the dropdown alone names the engine', async () => {
|
|
275
|
+
const base = await serve({ ELEVENLABS_API_KEY: 'testkey', ELEVENLABS_VOICE: 'voiceBBBBBBBBBBBBBBBB' });
|
|
276
|
+
const html = await (await fetch(`${base}/fragments/audio/new`)).text();
|
|
277
|
+
assert.match(html, /<option value="elevenlabs:voiceBBBBBBBBBBBBBBBB" selected>/, 'the default voice is chosen');
|
|
278
|
+
assert.ok(!/<option value="piper:[^"]*" selected>/.test(html));
|
|
279
|
+
assert.match(html, /<div data-engine-pane="elevenlabs">/, 'so its model and cost note show');
|
|
280
|
+
assert.match(html, /data-engine-pane="piper" hidden/);
|
|
281
|
+
assert.match(html, /arm-step" data-engine-button="elevenlabs" data-mode-button="text"(?! hidden)/, 'and its button, which asks first');
|
|
282
|
+
const found = await engines({ ELEVENLABS_API_KEY: 'testkey' });
|
|
283
|
+
const el = voice.plan({ text: 'Hi', voice_choice: 'elevenlabs:voiceAAAAAAAAAAAAAAAA', el_model: 'eleven_v4' }, found);
|
|
284
|
+
assert.deepStrictEqual([el.engine, el.voice, el.model], ['elevenlabs', 'voiceAAAAAAAAAAAAAAAA', 'eleven_v4']);
|
|
285
|
+
const pp = voice.plan({ text: 'Hi', voice_choice: 'piper:en_US-lessac-high' }, found);
|
|
286
|
+
assert.deepStrictEqual([pp.engine, pp.voice], ['piper', 'en_US-lessac-high']);
|
|
287
|
+
assert.match(voice.plan({ text: 'Hi', voice_choice: 'elevenlabs:nobody' }, found).error, /not on this account/);
|
|
288
|
+
assert.match(voice.plan({ text: 'Hi', voice_choice: 'nonsense:x' }, found).error, /Choose a voice/);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('the generate action refuses what the form should never send', async () => {
|
|
292
|
+
const base = await serve({ ELEVENLABS_API_KEY: 'testkey' });
|
|
293
|
+
const send = (form) => post(base, '/actions/generate-voice', { workspace: 'audio', ...form });
|
|
294
|
+
assert.strictEqual((await send({ text: '', engine: 'piper' })).status, 400);
|
|
295
|
+
assert.strictEqual((await send({ text: 'Hi', engine: 'nope' })).status, 400);
|
|
296
|
+
const spend = await send({ text: 'Hi', engine: 'elevenlabs', el_voice: 'voiceAAAAAAAAAAAAAAAA' });
|
|
297
|
+
assert.strictEqual(spend.status, 400);
|
|
298
|
+
assert.match(await spend.text(), /needs your confirmation/);
|
|
299
|
+
assert.strictEqual((await post(base, '/actions/generate-voice', { workspace: 'dev', text: 'Hi', engine: 'piper' })).status, 400);
|
|
300
|
+
const leftovers = () => { const d = path.join(root, '.screens', 'voice-text'); return fs.existsSync(d) ? fs.readdirSync(d) : []; };
|
|
301
|
+
const before = leftovers().length;
|
|
302
|
+
const ok = await send({ text: 'Hi', engine: 'elevenlabs', el_voice: 'voiceAAAAAAAAAAAAAAAA', confirm: 'yes' });
|
|
303
|
+
assert.ok([200, 409, 500].includes(ok.status));
|
|
304
|
+
assert.match(await ok.text(), /voice|runner/i);
|
|
305
|
+
if (ok.status === 500) assert.strictEqual(leftovers().length, before, 'the text is not left behind when nothing started');
|
|
306
|
+
// And where it is written is a place the host can read: under the workspace, not the container's /tmp.
|
|
307
|
+
const made = voice.writeText('Hi');
|
|
308
|
+
assert.ok(made.startsWith(path.join(root, '.screens', 'voice-text')), made);
|
|
309
|
+
assert.strictEqual(fs.readFileSync(made, 'utf8'), 'Hi\n');
|
|
310
|
+
assert.strictEqual(fs.statSync(path.dirname(made)).mode & 0o077, 0, 'private to its owner');
|
|
311
|
+
fs.rmSync(path.dirname(made), { recursive: true, force: true });
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('the video mode picks from the videos and scripts already in the workspace', () => {
|
|
315
|
+
const have = voice.choices();
|
|
316
|
+
assert.deepStrictEqual(have.videos, ['demo.mp4']);
|
|
317
|
+
assert.deepStrictEqual(have.scripts, ['demo-voiceover-script.md']);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('a timed script is read by section, and a plain transcript as one block', () => {
|
|
321
|
+
const timed = voice.parseScript(TIMED);
|
|
322
|
+
assert.strictEqual(timed.timed, true);
|
|
323
|
+
assert.deepStrictEqual(timed.sections.map((x) => [x.id, x.window, x.chars]), [['S01', 10, 45], ['S02', 10, 23]]);
|
|
324
|
+
const plain = voice.parseScript('- First point.\n2. Second point.\n# A heading\n');
|
|
325
|
+
assert.deepStrictEqual([plain.timed, plain.characters], [false, 'First point. Second point.'.length]);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('a video voiceover is planned from a chosen script, or from pasted text, before anything runs', async () => {
|
|
329
|
+
const found = await engines({ ELEVENLABS_API_KEY: 'testkey' });
|
|
330
|
+
const form = { kind: 'video', engine: 'elevenlabs', el_voice: 'voiceAAAAAAAAAAAAAAAA', video: 'demo.mp4', script_file: 'demo-voiceover-script.md' };
|
|
331
|
+
const p = voice.plan(form, found);
|
|
332
|
+
assert.deepStrictEqual([p.kind, p.file, p.characters, p.parsed.timed], ['video', 'demo-elevenlabs-voice.mp4', 68, true]);
|
|
333
|
+
const line = voice.commandLine(p, null, true);
|
|
334
|
+
assert.match(line, /"-a" "voiceover" "--engine" "elevenlabs" "--script" "[^"]*docs\/demo-voiceover-script\.md" "--video" "[^"]*videos\/demo\.mp4" "--out" "[^"]*videos\/demo-elevenlabs-voice\.mp4"/);
|
|
335
|
+
assert.match(line, /"--confirm"/);
|
|
336
|
+
assert.ok(!/[$`]/.test(line) && !/rm -rf/.test(line), 'a script read in place is never deleted');
|
|
337
|
+
|
|
338
|
+
const pasted = voice.plan({ ...form, script_file: '', text: 'One plain sentence.', name: 'My cut' }, found);
|
|
339
|
+
assert.deepStrictEqual([pasted.file, pasted.parsed.timed, pasted.scriptLabel], ['my-cut.mp4', false, 'pasted text']);
|
|
340
|
+
const both = voice.plan({ ...form, text: 'Typed earlier, in text mode.' }, found);
|
|
341
|
+
assert.deepStrictEqual([both.scriptLabel, both.parsed.timed, both.characters], ['demo-voiceover-script.md', true, 68], 'a chosen script wins over typed text');
|
|
342
|
+
assert.match(voice.commandLine(pasted, '/tmp/ws-voice-x/text.txt', true), /"--script" "\/tmp\/ws-voice-x\/text\.txt".*rm -rf "\/tmp\/ws-voice-x"/);
|
|
343
|
+
|
|
344
|
+
const err = (f) => voice.plan({ ...form, ...f }, found).error;
|
|
345
|
+
assert.match(err({ video: 'other.mp4' }), /Choose a video/);
|
|
346
|
+
assert.match(err({ script_file: '' }), /Choose a script, or paste one/);
|
|
347
|
+
assert.match(err({ script_file: 'notes.md' }), /Choose a script, or paste one/);
|
|
348
|
+
assert.match(err({ script_file: '', text: '#### only a heading' }), /nothing to say/);
|
|
349
|
+
assert.match(err({ script_file: '', text: 'x'.repeat(voice.MAX_SCRIPT + 1) }), /limit is/);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('the plan shows the sections and the credits, and the spending button asks first', async () => {
|
|
353
|
+
const base = await serve({ ELEVENLABS_API_KEY: 'testkey' });
|
|
354
|
+
const send = (form) => post(base, '/actions/voiceover-plan', { workspace: 'audio', video: 'demo.mp4', script_file: 'demo-voiceover-script.md', ...form });
|
|
355
|
+
const html = await (await send({ engine: 'elevenlabs', el_voice: 'voiceAAAAAAAAAAAAAAAA' })).text();
|
|
356
|
+
assert.match(html, /script <code>demo-voiceover-script\.md<\/code>/);
|
|
357
|
+
assert.match(html, /<td>S01<\/td><td>Opening<\/td>/);
|
|
358
|
+
assert.match(html, /68<\/strong> characters of the 1,000 left on the creator plan/);
|
|
359
|
+
assert.match(html, /arm-step[\s\S]*hx-vals='\{"confirm":"yes"\}'[\s\S]*hx-trigger="confirmed-remove"/);
|
|
360
|
+
const local = await (await send({ engine: 'piper', piper_voice: 'en_US-lessac-high' })).text();
|
|
361
|
+
assert.match(local, /Piper is free/);
|
|
362
|
+
assert.ok(!/arm-step/.test(local));
|
|
363
|
+
assert.strictEqual((await send({ engine: 'piper', piper_voice: 'en_US-lessac-high', video: 'gone.mp4' })).status, 400);
|
|
364
|
+
const form = await (await fetch(`${base}/fragments/audio/new?mode=video`)).text();
|
|
365
|
+
assert.match(form, /<input type="hidden" name="kind" value="video">/);
|
|
366
|
+
assert.match(form, /<option value="demo\.mp4" selected>/);
|
|
367
|
+
assert.match(form, /<option value="demo-voiceover-script\.md">/);
|
|
368
|
+
assert.match(form, /data-plan-button hx-post="\/actions\/voiceover-plan"/);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('Piper speaks a script over a video, and spreads a plain transcript across it', { skip: !hasFfmpeg() }, async () => {
|
|
372
|
+
const bin = path.join(home, 'fakepiper');
|
|
373
|
+
fs.writeFileSync(bin, '#!/usr/bin/env bash\nwhile [ $# -gt 0 ]; do [ "$1" = --output_file ] && out="$2"; shift; done\ncat > /dev/null\nffmpeg -y -loglevel error -f lavfi -i "sine=frequency=440:duration=1.5" "$out"\n', { mode: 0o755 });
|
|
374
|
+
fs.mkdirSync(path.join(home, '.local/share/piper-voices'), { recursive: true });
|
|
375
|
+
fs.writeFileSync(path.join(home, '.local/share/piper-voices/en_US-lessac-high.onnx'), 'x');
|
|
376
|
+
const clip = path.join(root, 'videos/clip.mp4');
|
|
377
|
+
await run('ffmpeg', ['-y', '-loglevel', 'error', '-f', 'lavfi', '-i', 'testsrc=duration=6:size=160x120:rate=10', '-f', 'lavfi', '-i', 'anullsrc=r=44100:cl=mono', '-shortest',
|
|
378
|
+
'-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-c:a', 'aac', clip]);
|
|
379
|
+
const transcript = path.join(home, 'transcript.txt');
|
|
380
|
+
fs.writeFileSync(transcript, 'A plain transcript.\nTwo short lines.\n');
|
|
381
|
+
const out = path.join(root, 'videos/clip-voiced.mp4');
|
|
382
|
+
const env = baseEnv({ PIPER_BIN: bin, ELEVENLABS_API_KEY: '' });
|
|
383
|
+
const go = (extra) => run('bash', [path.join(root, 'audio/cmd-tools-voice.sh'), '-a', 'voiceover', '--engine', 'piper', '--script', transcript, '--video', clip, '--out', out,
|
|
384
|
+
'--voice', 'en_US-lessac-high', ...extra], { env }).catch((e) => e);
|
|
385
|
+
const plan = await go([]);
|
|
386
|
+
assert.strictEqual(plan.code, 4, 'without --confirm it only plans');
|
|
387
|
+
assert.ok(!fs.existsSync(out));
|
|
388
|
+
const done = await go(['--confirm']);
|
|
389
|
+
assert.ok(!done.code, String(done.stderr));
|
|
390
|
+
const probe = (await run('ffprobe', ['-v', 'error', '-show_entries', 'stream=codec_type:format=duration', '-of', 'compact', out])).stdout;
|
|
391
|
+
assert.match(probe, /codec_type=video/);
|
|
392
|
+
assert.match(probe, /codec_type=audio/);
|
|
393
|
+
assert.ok(Math.abs(parseFloat(probe.match(/duration=([\d.]+)/)[1]) - 6) < 0.3);
|
|
394
|
+
assert.deepStrictEqual(fs.readdirSync(os.tmpdir()).filter((n) => n.startsWith('ws-voice-script-')), [], 'the temporary script is removed');
|
|
395
|
+
const again = await go(['--confirm']);
|
|
396
|
+
assert.notStrictEqual(again.code, undefined, 'an existing output is refused');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
function hasFfmpeg() {
|
|
400
|
+
try { require('child_process').execFileSync('ffmpeg', ['-version'], { stdio: 'ignore' }); return true; } catch (_) { return false; }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
// ---- sounds, music, and what belongs to a generated file ----------------------------------------
|
|
405
|
+
|
|
406
|
+
const cli = () => path.join(root, 'audio/cmd-tools-voice.sh');
|
|
407
|
+
const say = (args, extra) => run('bash', [cli(), ...args], { env: baseEnv({ ELEVENLABS_API_KEY: 'testkey', ...extra }) }).catch((e) => e);
|
|
408
|
+
|
|
409
|
+
test('a sound is planned first, made only with --confirm, and kept with its text and record', async () => {
|
|
410
|
+
const text = path.join(home, 'sound.txt');
|
|
411
|
+
fs.writeFileSync(text, 'Rain on a tin roof');
|
|
412
|
+
const out = path.join(home, 'rain.mp3');
|
|
413
|
+
const base = ['-a', 'sound', '--text-file', text, '--out', out, '--duration', '4', '--loop'];
|
|
414
|
+
const before = calls.length;
|
|
415
|
+
const plan = await say(base);
|
|
416
|
+
assert.strictEqual(plan.code, 4);
|
|
417
|
+
assert.match(plan.stdout, /Plan only, nothing was sent\./);
|
|
418
|
+
assert.strictEqual(calls.length, before, 'nothing was sent');
|
|
419
|
+
const done = await say([...base, '--confirm']);
|
|
420
|
+
assert.ok(!done.code, String(done.stderr));
|
|
421
|
+
const sent = calls[calls.length - 1];
|
|
422
|
+
assert.strictEqual(sent.url.split('?')[0], '/v1/sound-generation');
|
|
423
|
+
assert.deepStrictEqual(sent.body, { text: 'Rain on a tin roof', prompt_influence: 0.3, duration_seconds: 4, loop: true });
|
|
424
|
+
assert.strictEqual(fs.statSync(out).size, 900);
|
|
425
|
+
assert.strictEqual(fs.readFileSync(path.join(home, 'rain.txt'), 'utf8'), 'Rain on a tin roof\n');
|
|
426
|
+
const record = JSON.parse(fs.readFileSync(path.join(home, 'rain.json'), 'utf8'));
|
|
427
|
+
assert.deepStrictEqual([record.kind, record.engine, record.duration, record.loop, record.file], ['sound', 'elevenlabs', 4, true, 'rain.mp3']);
|
|
428
|
+
assert.match(record.created_utc, /^\d{4}-\d\d-\d\dT/);
|
|
429
|
+
assert.match((await say([...base, '--confirm'])).stderr, /already exists/);
|
|
430
|
+
assert.match((await say(['-a', 'sound', '--text-file', text, '--out', path.join(home, 'x.mp3'), '--duration', '99'])).stderr, /0\.5 to 30/);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('music asks for its length and whether it may have vocals', async () => {
|
|
434
|
+
const text = path.join(home, 'music.txt');
|
|
435
|
+
fs.writeFileSync(text, 'A calm piano loop');
|
|
436
|
+
const out = path.join(home, 'calm.mp3');
|
|
437
|
+
const done = await say(['-a', 'music', '--text-file', text, '--out', out, '--length', '12', '--instrumental', '--confirm']);
|
|
438
|
+
assert.ok(!done.code, String(done.stderr));
|
|
439
|
+
const sent = calls[calls.length - 1];
|
|
440
|
+
assert.strictEqual(sent.url.split('?')[0], '/v1/music');
|
|
441
|
+
assert.deepStrictEqual(sent.body, { prompt: 'A calm piano loop', music_length_ms: 12000, force_instrumental: true });
|
|
442
|
+
assert.strictEqual(JSON.parse(fs.readFileSync(path.join(home, 'calm.json'), 'utf8')).instrumental, true);
|
|
443
|
+
assert.match((await say(['-a', 'music', '--text-file', text, '--out', path.join(home, 'y.mp3'), '--length', '1'])).stderr, /3 to 600/);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('a spoken voice also keeps its text and a record beside it', async () => {
|
|
447
|
+
const text = path.join(home, 'voice.txt');
|
|
448
|
+
fs.writeFileSync(text, 'Hello there');
|
|
449
|
+
const out = path.join(home, 'hello.mp3');
|
|
450
|
+
const done = await say(['-a', 'speak', '--engine', 'elevenlabs', '--text-file', text, '--out', out, '--voice', 'voiceAAAAAAAAAAAAAAAA', '--confirm']);
|
|
451
|
+
assert.ok(!done.code, String(done.stderr));
|
|
452
|
+
const record = JSON.parse(fs.readFileSync(path.join(home, 'hello.json'), 'utf8'));
|
|
453
|
+
assert.deepStrictEqual([record.kind, record.engine, record.voice], ['voice', 'elevenlabs', 'voiceAAAAAAAAAAAAAAAA']);
|
|
454
|
+
assert.strictEqual(fs.readFileSync(path.join(home, 'hello.txt'), 'utf8'), 'Hello there\n');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test('sounds and music are planned from the form, into audio/sounds', async () => {
|
|
458
|
+
const found = await engines({ ELEVENLABS_API_KEY: 'testkey' });
|
|
459
|
+
const sound = voice.plan({ kind: 'sound', text: 'A door creaks', duration: '3', influence: '0.7', loop: 'yes' }, found);
|
|
460
|
+
assert.deepStrictEqual([sound.kind, sound.duration, sound.influence, sound.loop, sound.file], ['sound', 3, 0.7, true, 'a-door-creaks-sound.mp3']);
|
|
461
|
+
const music = voice.plan({ kind: 'music', text: 'Calm', length: '60', instrumental: 'yes', name: 'Theme' }, found);
|
|
462
|
+
assert.deepStrictEqual([music.kind, music.length, music.instrumental, music.file], ['music', 60, true, 'theme.mp3']);
|
|
463
|
+
const auto = voice.plan({ kind: 'sound', text: 'A bell', duration: 'auto' }, found);
|
|
464
|
+
assert.deepStrictEqual([auto.duration, auto.influence, auto.loop], [null, 0.3, false]);
|
|
465
|
+
assert.match(voice.plan({ kind: 'sound', text: '' }, found).error, /Describe the sound/);
|
|
466
|
+
assert.match(voice.plan({ kind: 'music', text: 'x', length: '2' }, found).error, /3 to 600/);
|
|
467
|
+
assert.match(voice.plan({ kind: 'sound', text: 'x', duration: '60' }, found).error, /0\.5 to 30/);
|
|
468
|
+
assert.match(voice.plan({ kind: 'sound', text: 'x'.repeat(voice.MAX_PROMPT + 1) }, found).error, /at most/);
|
|
469
|
+
assert.match(voice.plan({ kind: 'sound', text: 'A bell' }, await engines({ ELEVENLABS_API_KEY: '' })).error, /not available/);
|
|
470
|
+
const line = voice.commandLine(sound, '/tmp/x/text.txt', true);
|
|
471
|
+
assert.match(line, /"-a" "sound" "--text-file" "[^"]*" "--out" "[^"]*audio\/sounds\/a-door-creaks-sound\.mp3" "--influence" "0\.7" "--duration" "3" "--loop" "--confirm"/);
|
|
472
|
+
assert.ok(!/[$`]/.test(line));
|
|
473
|
+
assert.match(voice.commandLine(music, '/tmp/x/text.txt', true), /"-a" "music"[^]*"--length" "60" "--instrumental" "--confirm"/);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test('each action opens its own modal: voice, voice over a video, a sound, music', async () => {
|
|
477
|
+
const base = await serve({ ELEVENLABS_API_KEY: 'testkey' });
|
|
478
|
+
const page = await (await fetch(`${base}/audio`)).text();
|
|
479
|
+
for (const mode of ['voice', 'video', 'sound', 'music']) assert.ok(page.includes(`hx-get="/fragments/audio/new?mode=${mode}"`), mode);
|
|
480
|
+
for (const label of ['Generate voice', 'Voice over a video', 'Generate a sound', 'Generate music']) assert.ok(page.includes(label), label);
|
|
481
|
+
// One stroke down the whole rail: every entry is drawn from the Tabler sprite, one icon per meaning.
|
|
482
|
+
for (const icon of ['microphone', 'video', 'wave-sine', 'music']) assert.ok(page.includes(`tabler-sprite.svg#${icon}"`), `the ${icon} icon`);
|
|
483
|
+
assert.ok(!/uk-icon="icon: tabler:/.test(page.slice(page.indexOf('action-btn'))), 'no Tabler name in a UIkit icon slot');
|
|
484
|
+
for (const mode of ['voice', 'video', 'sound', 'music']) {
|
|
485
|
+
assert.match(page, new RegExp(`hx-get="/fragments/audio/new\\?mode=${mode}" hx-target="#editor-modal-body" hx-swap="innerHTML"`), `${mode} opens in the modal, not in the page`);
|
|
486
|
+
}
|
|
487
|
+
const form = async (mode) => (await fetch(`${base}/fragments/audio/new?mode=${mode}`)).text();
|
|
488
|
+
const voiceForm = await form('voice');
|
|
489
|
+
assert.match(voiceForm, /Generate a voice/);
|
|
490
|
+
assert.match(voiceForm, /<input type="hidden" name="kind" value="text">/);
|
|
491
|
+
assert.ok(!/name="video"|data-plan-button|name="kind" value="video"/.test(voiceForm));
|
|
492
|
+
const videoForm = await form('video');
|
|
493
|
+
assert.match(videoForm, /Voice over a video/);
|
|
494
|
+
assert.match(videoForm, /<input type="hidden" name="kind" value="video">/);
|
|
495
|
+
assert.match(videoForm, /name="video"/);
|
|
496
|
+
assert.match(videoForm, /data-plan-button/);
|
|
497
|
+
assert.match(videoForm, /data-engine-button="piper" data-mode-button="text" hidden/);
|
|
498
|
+
const soundForm = await form('sound');
|
|
499
|
+
assert.match(soundForm, /Generate a sound/);
|
|
500
|
+
assert.match(soundForm, /name="kind" value="sound"/);
|
|
501
|
+
assert.match(soundForm, /name="duration"[\s\S]*name="influence"[\s\S]*name="loop"/);
|
|
502
|
+
assert.match(soundForm, /arm-step[^>]*hx-post="\/actions\/generate-voice"[\s\S]*hx-vals='\{"confirm":"yes"\}'/);
|
|
503
|
+
assert.match(soundForm, /1,000 credits left/);
|
|
504
|
+
const musicForm = await form('music');
|
|
505
|
+
assert.match(musicForm, /name="length"[\s\S]*name="instrumental" value="yes" checked/);
|
|
506
|
+
assert.strictEqual(await form('nonsense').then((h) => /Generate a voice/.test(h)), true, 'an unknown mode is the voice form');
|
|
507
|
+
const bare = await serve({ ELEVENLABS_API_KEY: '' });
|
|
508
|
+
const refused = await (await fetch(`${bare}/fragments/audio/new?mode=sound`)).text();
|
|
509
|
+
assert.match(refused, /Sounds come from ElevenLabs, which is not available/);
|
|
510
|
+
assert.ok(!/arm-step/.test(refused));
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test('a sound or music request without confirmation is refused', async () => {
|
|
514
|
+
const base = await serve({ ELEVENLABS_API_KEY: 'testkey' });
|
|
515
|
+
const r = await post(base, '/actions/generate-voice', { workspace: 'audio', kind: 'sound', text: 'A bell', engine: 'elevenlabs' });
|
|
516
|
+
assert.strictEqual(r.status, 400);
|
|
517
|
+
assert.match(await r.text(), /needs your confirmation/);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// ---- generating inside the modal -----------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
const screens = () => path.join(root, '.screens');
|
|
523
|
+
function hostSays(sessions) {
|
|
524
|
+
fs.mkdirSync(screens(), { recursive: true });
|
|
525
|
+
fs.writeFileSync(path.join(screens(), 'host-sessions.json'), JSON.stringify({ at: Date.now(), host: 'test', sessions: sessions.map((session) => ({ session })) }));
|
|
526
|
+
}
|
|
527
|
+
const gen = (base, q) => fetch(`${base}/fragments/audio/generation?${new URLSearchParams(q)}`);
|
|
528
|
+
|
|
529
|
+
test('the forms keep the work in the modal: their results go to #voice-result', async () => {
|
|
530
|
+
const base = await serve({ ELEVENLABS_API_KEY: 'testkey' });
|
|
531
|
+
for (const mode of ['voice', 'video', 'sound', 'music']) {
|
|
532
|
+
const html = await (await fetch(`${base}/fragments/audio/new?mode=${mode}`)).text();
|
|
533
|
+
assert.ok(html.includes('<div id="voice-result"'), `${mode} has a place for the result`);
|
|
534
|
+
assert.ok(!html.includes('#webship-workspace-output'), `${mode} no longer answers in a toast`);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test('while a voice is made the modal says so and asks again; when the file is there it shows it', async () => {
|
|
539
|
+
const base = await serve({ ELEVENLABS_API_KEY: '' });
|
|
540
|
+
const q = { kind: 'text', file: 'modal-test.wav', session: 'audio--modal-test--voice', t0: String(Date.now()), engine: 'piper' };
|
|
541
|
+
hostSays(['audio--modal-test--voice']);
|
|
542
|
+
const running = await (await gen(base, q)).text();
|
|
543
|
+
assert.match(running, /class="voice-progress" hx-get="\/fragments\/audio\/generation\?kind=text&file=modal-test\.wav/);
|
|
544
|
+
assert.match(running, /hx-trigger="load delay:1s" hx-swap="outerHTML"/);
|
|
545
|
+
assert.match(running, /Making <strong>modal-test\.wav<\/strong> with Piper/);
|
|
546
|
+
fs.mkdirSync(path.join(root, '.screens'), { recursive: true });
|
|
547
|
+
fs.writeFileSync(path.join(screens(), 'audio--modal-test--voice.log'), '\x1b[32mloading\x1b[0m the voice\r\nspeaking\n');
|
|
548
|
+
assert.match(await (await gen(base, q)).text(), /<pre class="voice-log">loading the voice\nspeaking<\/pre>/, 'the log, without terminal escapes');
|
|
549
|
+
const dir = path.join(root, 'audio/voices');
|
|
550
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
551
|
+
fs.writeFileSync(path.join(dir, 'modal-test.wav'), Buffer.alloc(100, 1));
|
|
552
|
+
assert.match(await (await gen(base, q)).text(), /voice-progress/, 'the file alone is not enough while the record is still being written');
|
|
553
|
+
fs.writeFileSync(path.join(dir, 'modal-test.txt'), 'Hello from the modal\n');
|
|
554
|
+
fs.writeFileSync(path.join(dir, 'modal-test.json'), JSON.stringify({ kind: 'voice', engine: 'piper', voice: 'en_GB-cori-high' }));
|
|
555
|
+
const done = await gen(base, q);
|
|
556
|
+
assert.strictEqual(done.headers.get('hx-trigger'), 'refresh-projects', 'the list behind the modal refreshes');
|
|
557
|
+
const html = await done.text();
|
|
558
|
+
assert.match(html, /<strong>modal-test\.wav<\/strong> is ready, in <code>audio\/voices\/<\/code>/);
|
|
559
|
+
assert.match(html, /<audio class="sound-player" controls preload="metadata" src="\/files\/audio\/voices\/modal-test\.wav">/);
|
|
560
|
+
assert.match(html, /Hello from the modal/);
|
|
561
|
+
assert.match(html, /With<\/span> piper/);
|
|
562
|
+
assert.match(html, /hx-get="\/fragments\/audio\/new\?mode=voice" hx-target="#editor-modal-body"/);
|
|
563
|
+
assert.ok(!/voice-progress/.test(html));
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test('a generation whose session is gone and left no file says why', async () => {
|
|
567
|
+
const base = await serve({ ELEVENLABS_API_KEY: '' });
|
|
568
|
+
const q = { kind: 'sound', file: 'never.mp3', session: 'audio--never--sound', t0: String(Date.now() - 20000), engine: 'elevenlabs' };
|
|
569
|
+
hostSays([]);
|
|
570
|
+
fs.mkdirSync(screens(), { recursive: true });
|
|
571
|
+
fs.writeFileSync(path.join(screens(), 'audio--never--sound.log'), 'Sound generation failed. HTTP 401. ElevenLabs rejected the key.\n');
|
|
572
|
+
const r = await gen(base, q);
|
|
573
|
+
assert.notStrictEqual(r.headers.get('hx-trigger'), 'refresh-projects');
|
|
574
|
+
const html = await r.text();
|
|
575
|
+
assert.match(html, /msg error voice-failed/);
|
|
576
|
+
assert.match(html, /<strong>never\.mp3<\/strong> was not made/);
|
|
577
|
+
assert.match(html, /ElevenLabs rejected the key\./);
|
|
578
|
+
// The session is still listed, or it only just started: not a failure yet.
|
|
579
|
+
hostSays(['audio--never--sound']);
|
|
580
|
+
assert.match(await (await gen(base, q)).text(), /voice-progress/);
|
|
581
|
+
hostSays([]);
|
|
582
|
+
assert.match(await (await gen(base, { ...q, t0: String(Date.now()) })).text(), /voice-progress/, 'a grace period before it is called failed');
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test('what the modal asks for is checked: a kind, a plain file name, a session name', async () => {
|
|
586
|
+
const base = await serve({ ELEVENLABS_API_KEY: '' });
|
|
587
|
+
const ok = { kind: 'text', file: 'a.wav', session: 'audio--a--voice', t0: String(Date.now()), engine: 'piper' };
|
|
588
|
+
hostSays(['audio--a--voice']);
|
|
589
|
+
assert.strictEqual((await gen(base, ok)).status, 200);
|
|
590
|
+
for (const bad of [{ kind: 'dir' }, { kind: 'text', file: '../secret.wav' }, { file: 'a/b.wav' }, { file: '.hidden.wav' }, { session: 'Audio--A' }, { session: '../x' }, { file: '' }]) {
|
|
591
|
+
assert.strictEqual((await gen(base, { ...ok, ...bad })).status, 400, JSON.stringify(bad));
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test('pressing Generate answers with the progress, once the host runner has started the session', async () => {
|
|
596
|
+
fs.mkdirSync(path.join(screens(), 'queue'), { recursive: true });
|
|
597
|
+
fs.mkdirSync(path.join(screens(), 'results'), { recursive: true });
|
|
598
|
+
const asked = [];
|
|
599
|
+
const runner = setInterval(() => {
|
|
600
|
+
hostSays([]);
|
|
601
|
+
for (const f of fs.readdirSync(path.join(screens(), 'queue'))) {
|
|
602
|
+
if (!f.endsWith('.json')) continue;
|
|
603
|
+
const req = JSON.parse(fs.readFileSync(path.join(screens(), 'queue', f), 'utf8'));
|
|
604
|
+
fs.unlinkSync(path.join(screens(), 'queue', f));
|
|
605
|
+
asked.push(req);
|
|
606
|
+
fs.writeFileSync(path.join(screens(), 'results', `${req.id}.json`), JSON.stringify({ ok: true }));
|
|
607
|
+
}
|
|
608
|
+
}, 80);
|
|
609
|
+
try {
|
|
610
|
+
const base = await serve({ ELEVENLABS_API_KEY: '' });
|
|
611
|
+
const found = await engines({ ELEVENLABS_API_KEY: '' });
|
|
612
|
+
const piper = found[0].voices[0].id;
|
|
613
|
+
const r = await post(base, '/actions/generate-voice', { workspace: 'audio', kind: 'text', engine: 'piper', piper_voice: piper, text: 'Hello from a test', name: 'press-test' });
|
|
614
|
+
assert.strictEqual(r.status, 200);
|
|
615
|
+
const html = await r.text();
|
|
616
|
+
assert.match(html, /class="voice-progress" hx-get="\/fragments\/audio\/generation\?kind=text&file=press-test\.wav&session=audio--press-test--voice&t0=\d+&engine=piper&edit="/);
|
|
617
|
+
assert.match(html, /Making <strong>press-test\.wav<\/strong> with Piper/);
|
|
618
|
+
assert.strictEqual(asked.length, 1);
|
|
619
|
+
assert.match(asked[0].cmd, /"-a" "speak" "--engine" "piper"/);
|
|
620
|
+
assert.ok(!/<div class="msg/.test(html), 'a progress card, not a toast');
|
|
621
|
+
} finally { clearInterval(runner); }
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test('the tests run on placeholder ElevenLabs settings, whatever the machine\'s own settings.yml holds', () => {
|
|
625
|
+
const { withoutElevenlabs, PLACEHOLDERS } = require('./_isolated-config');
|
|
626
|
+
const machine = '# settings\nworkspaces:\n - dev\nelevenlabs:\n api_key: sk_realkey\n voice: Brian\n model: eleven_multilingual_v2\n\nstyle:\n mode: light\n';
|
|
627
|
+
const clean = withoutElevenlabs(machine);
|
|
628
|
+
assert.ok(!/sk_realkey|Brian|multilingual/.test(clean), 'the machine\'s values are gone');
|
|
629
|
+
assert.match(clean, /workspaces:\n {2}- dev\n/);
|
|
630
|
+
assert.match(clean, /style:\n {2}mode: light/, 'what follows the block is kept');
|
|
631
|
+
const used = fs.readFileSync(path.join(root, 'core/config/settings.yml'), 'utf8');
|
|
632
|
+
assert.ok(used.includes(PLACEHOLDERS), 'the copy the tests use carries the placeholders');
|
|
633
|
+
assert.ok(!/api_key: sk_/.test(used.slice(used.indexOf('elevenlabs:'))), 'and no key');
|
|
634
|
+
});
|