@webship.co/workspace 1.0.0-alpha1 → 1.0.0-beta2
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} +1 -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/{plugin-loader.js → core/workspace-app/plugin-loader.js} +19 -1
- package/{plugins-page.js → core/workspace-app/plugins-page.js} +28 -2
- package/{row-layout.js → core/workspace-app/row-layout.js} +0 -1
- package/{server.js → core/workspace-app/server.js} +26 -225
- 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 +39 -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 +40 -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 +152 -0
- package/core/workspace-app/tests/step-definitions/preconditions.steps.js +100 -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 +177 -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 +153 -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/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 +74 -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 +198 -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 +417 -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/{workspace-types.js → core/workspace-app/workspace-types.js} +2 -2
- package/{workspaces.js → core/workspace-app/workspaces.js} +1 -5
- package/package.json +38 -36
- package/actions/graphs.js +0 -107
- package/actions/obsidian.js +0 -59
- package/graphs.js +0 -104
- package/obsidian.js +0 -405
- /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-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/{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
|
@@ -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
|
+
});
|