@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,627 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ElevenLabs for the final voiceover of a narrated video. Standard library only.
|
|
3
|
+
|
|
4
|
+
Run through videos/cmd-tools-elevenlabs.sh, which reads the key from settings.yml and hands it over in
|
|
5
|
+
ELEVENLABS_API_KEY. Anything that spends credits prints a plan and stops unless --confirm is given.
|
|
6
|
+
Exit codes: 0 ok, 1 error, 2 usage, 3 no key, 4 plan only, 5 sections do not fit their windows.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import datetime
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.parse
|
|
20
|
+
import urllib.request
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
23
|
+
OK, ERROR, USAGE, NO_KEY, PLAN_ONLY, NO_FIT = 0, 1, 2, 3, 4, 5
|
|
24
|
+
DEFAULT_BASE = 'https://api.elevenlabs.io'
|
|
25
|
+
DEFAULT_MODEL = 'eleven_v4'
|
|
26
|
+
DEFAULT_FORMAT = 'mp3_44100_128'
|
|
27
|
+
SPEECH_CHARS_PER_SECOND = 15.0
|
|
28
|
+
SLACK = 0.05
|
|
29
|
+
|
|
30
|
+
HEADING = re.compile(
|
|
31
|
+
r'^##\s+(\S+)\s+(.+?)\s+\(((?:\d+:)?\d{1,2}:\d{2}(?:\.\d+)?)\s+to\s+((?:\d+:)?\d{1,2}:\d{2}(?:\.\d+)?)\)\s*$')
|
|
32
|
+
HINTS = {
|
|
33
|
+
401: 'ElevenLabs rejected the key.',
|
|
34
|
+
403: 'The key is not allowed to do that.',
|
|
35
|
+
404: 'Not found (check the voice id).',
|
|
36
|
+
429: 'ElevenLabs is rate limiting this key. Wait, then run it again; finished sections are kept.',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Stop(Exception):
|
|
41
|
+
def __init__(self, message, code=ERROR):
|
|
42
|
+
super().__init__(message)
|
|
43
|
+
self.code = code
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def emit(args, data, lines):
|
|
47
|
+
print(json.dumps(data, indent=2) if args.json else '\n'.join(lines))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def mmss(seconds):
|
|
51
|
+
return '%02d:%04.1f' % (int(seconds // 60), seconds % 60)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def seconds_of(stamp):
|
|
55
|
+
total = 0.0
|
|
56
|
+
for part in stamp.split(':'):
|
|
57
|
+
total = total * 60 + float(part)
|
|
58
|
+
return total
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def sha(*parts):
|
|
62
|
+
return hashlib.sha256(json.dumps(parts, sort_keys=True).encode()).hexdigest()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def stem_of(path):
|
|
66
|
+
return os.path.splitext(os.path.basename(path))[0]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def safe_id(text):
|
|
70
|
+
return re.sub(r'[^A-Za-z0-9_-]', '_', text)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read_bytes(path):
|
|
74
|
+
with open(path, 'rb') as handle:
|
|
75
|
+
return handle.read()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def read_json(path):
|
|
79
|
+
with open(path, encoding='utf-8') as handle:
|
|
80
|
+
return json.load(handle)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def ensure_free(paths, force):
|
|
84
|
+
for path in paths:
|
|
85
|
+
if os.path.exists(path) and not force:
|
|
86
|
+
raise Stop('%s already exists. Pass --force to replace it. Nothing was sent.' % path)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def track_of(out):
|
|
90
|
+
return os.path.splitext(out)[0] + '-voiceover.mp3'
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def write_atomic(path, data, force):
|
|
94
|
+
if os.path.exists(path) and not force:
|
|
95
|
+
raise Stop('%s already exists. Pass --force to replace it.' % path)
|
|
96
|
+
folder = os.path.dirname(os.path.abspath(path))
|
|
97
|
+
os.makedirs(folder, exist_ok=True)
|
|
98
|
+
part = os.path.join(folder, '.%s.%s.part' % (os.path.basename(path), uuid.uuid4().hex[:8]))
|
|
99
|
+
with open(part, 'wb') as handle:
|
|
100
|
+
handle.write(data)
|
|
101
|
+
os.replace(part, path)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---- the API
|
|
105
|
+
|
|
106
|
+
def key():
|
|
107
|
+
value = os.environ.get('ELEVENLABS_API_KEY', '').strip()
|
|
108
|
+
if not value:
|
|
109
|
+
raise Stop('No ElevenLabs key is set. Add it at /settings (ElevenLabs), or run: bash cmd-tools-elevenlabs.sh -a init', NO_KEY)
|
|
110
|
+
if not re.fullmatch(r'[A-Za-z0-9_.-]+', value):
|
|
111
|
+
raise Stop('The ElevenLabs key has characters a key never has. Check the value in settings.yml.', NO_KEY)
|
|
112
|
+
return value
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def call(method, path, body=None, content_type=None, query=None):
|
|
116
|
+
url = os.environ.get('ELEVENLABS_API_BASE', DEFAULT_BASE).rstrip('/') + path
|
|
117
|
+
if query:
|
|
118
|
+
url += '?' + urllib.parse.urlencode(query)
|
|
119
|
+
headers = {'xi-api-key': key()}
|
|
120
|
+
if content_type:
|
|
121
|
+
headers['Content-Type'] = content_type
|
|
122
|
+
request = urllib.request.Request(url, data=body, method=method, headers=headers)
|
|
123
|
+
try:
|
|
124
|
+
with urllib.request.urlopen(request, timeout=int(os.environ.get('ELEVENLABS_TIMEOUT', '300'))) as response:
|
|
125
|
+
return response.status, response.headers, response.read()
|
|
126
|
+
except urllib.error.HTTPError as error:
|
|
127
|
+
return error.code, error.headers, error.read()
|
|
128
|
+
except (urllib.error.URLError, TimeoutError, OSError) as error:
|
|
129
|
+
raise Stop('Could not reach ElevenLabs (%s).' % getattr(error, 'reason', error))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
ID_LIKE = re.compile(r'[A-Za-z0-9]{16,32}')
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def resolve_voice(value):
|
|
136
|
+
"""A voice given by name (Brian) becomes its id on this account; an id is left alone."""
|
|
137
|
+
if ID_LIKE.fullmatch(value):
|
|
138
|
+
return value
|
|
139
|
+
status, _, body = call('GET', '/v2/voices', query={'search': value, 'page_size': '30'})
|
|
140
|
+
if status != 200:
|
|
141
|
+
raise Stop('Could not look up the voice "%s". %s' % (value, explain(status, body)))
|
|
142
|
+
voices = json.loads(body).get('voices', [])
|
|
143
|
+
want = value.strip().lower()
|
|
144
|
+
hit = (next((v for v in voices if (v.get('name') or '').lower() == want), None)
|
|
145
|
+
or next((v for v in voices if (v.get('name') or '').lower().startswith(want)), None))
|
|
146
|
+
if not hit:
|
|
147
|
+
raise Stop('No voice named "%s" on this account. List them with -a voices.' % value, USAGE)
|
|
148
|
+
return hit['voice_id']
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def explain(status, body):
|
|
152
|
+
detail = ''
|
|
153
|
+
try:
|
|
154
|
+
found = json.loads(body.decode('utf-8', 'replace')).get('detail')
|
|
155
|
+
if isinstance(found, dict):
|
|
156
|
+
detail = found.get('message') or found.get('status') or ''
|
|
157
|
+
elif isinstance(found, list):
|
|
158
|
+
detail = '; '.join(str(item.get('msg', item)) if isinstance(item, dict) else str(item) for item in found[:3])
|
|
159
|
+
elif found:
|
|
160
|
+
detail = str(found)
|
|
161
|
+
except Exception:
|
|
162
|
+
detail = body.decode('utf-8', 'replace')[:160].replace('\n', ' ')
|
|
163
|
+
hint = HINTS.get(status) or ('ElevenLabs had a server error; run it again later.' if status >= 500 else '')
|
|
164
|
+
return ' '.join(part for part in ('HTTP %d.' % status, hint, detail) if part)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def multipart(fields, files):
|
|
168
|
+
boundary = uuid.uuid4().hex
|
|
169
|
+
parts = []
|
|
170
|
+
for name, value in fields.items():
|
|
171
|
+
parts.append(('--%s\r\nContent-Disposition: form-data; name="%s"\r\n\r\n%s\r\n' % (boundary, name, value)).encode())
|
|
172
|
+
for name, filename, data in files:
|
|
173
|
+
parts.append(('--%s\r\nContent-Disposition: form-data; name="%s"; filename="%s"\r\n'
|
|
174
|
+
'Content-Type: application/octet-stream\r\n\r\n' % (boundary, name, filename)).encode() + data + b'\r\n')
|
|
175
|
+
parts.append(('--%s--\r\n' % boundary).encode())
|
|
176
|
+
return b''.join(parts), 'multipart/form-data; boundary=%s' % boundary
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def account():
|
|
180
|
+
status, _, body = call('GET', '/v1/user/subscription')
|
|
181
|
+
if status != 200:
|
|
182
|
+
return None, explain(status, body)
|
|
183
|
+
found = json.loads(body)
|
|
184
|
+
used, limit = found.get('character_count'), found.get('character_limit')
|
|
185
|
+
return {
|
|
186
|
+
'tier': found.get('tier'), 'status': found.get('status'), 'used': used, 'limit': limit,
|
|
187
|
+
'remaining': limit - used if isinstance(used, int) and isinstance(limit, int) else None,
|
|
188
|
+
'resets': found.get('next_character_count_reset_unix'),
|
|
189
|
+
}, ''
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def remaining_characters():
|
|
193
|
+
try:
|
|
194
|
+
found, _ = account()
|
|
195
|
+
except Stop:
|
|
196
|
+
return None
|
|
197
|
+
return found['remaining'] if found else None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# Brand names the voices misread: a client heard "Var-dough" for Webship in a final video. The respelling is
|
|
201
|
+
# applied ONLY to the text sent to the voice. Scripts, captions, transcripts, character counts and the
|
|
202
|
+
# saved section text keep the real spelling. ELEVENLABS_PRONUNCIATION ("Word=Respelling; Word=Respelling",
|
|
203
|
+
# from elevenlabs.pronunciation in settings.yml) adds to or overrides these.
|
|
204
|
+
PRONUNCIATIONS = {'Webship': 'Var-dot', 'Webship': 'Var-base'}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def pronunciations():
|
|
208
|
+
table = dict(PRONUNCIATIONS)
|
|
209
|
+
for pair in os.environ.get('ELEVENLABS_PRONUNCIATION', '').split(';'):
|
|
210
|
+
word, _, said = pair.partition('=')
|
|
211
|
+
if word.strip() and said.strip():
|
|
212
|
+
table[word.strip()] = said.strip()
|
|
213
|
+
return table
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def respell(text):
|
|
217
|
+
for word, said in pronunciations().items():
|
|
218
|
+
text = re.sub(r'(?<![\w-])%s(?![\w-])' % re.escape(word), said, text)
|
|
219
|
+
return text
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def synthesize(text, voice, model, fmt):
|
|
223
|
+
body = json.dumps({'text': respell(text), 'model_id': model}).encode()
|
|
224
|
+
status, headers, data = call('POST', '/v1/text-to-speech/' + urllib.parse.quote(voice, safe=''), body=body,
|
|
225
|
+
content_type='application/json', query={'output_format': fmt})
|
|
226
|
+
if status != 200 or not data or 'json' in (headers.get('content-type') or ''):
|
|
227
|
+
raise Stop('Speech failed. ' + explain(status, data))
|
|
228
|
+
return data, {'request_id': headers.get('request-id'), 'characters': headers.get('x-character-count')}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ---- media
|
|
232
|
+
|
|
233
|
+
def need_tools():
|
|
234
|
+
for tool in ('ffmpeg', 'ffprobe'):
|
|
235
|
+
if not shutil.which(tool):
|
|
236
|
+
raise Stop('%s is needed and was not found on PATH.' % tool)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def run(command):
|
|
240
|
+
done = subprocess.run(command, capture_output=True, text=True)
|
|
241
|
+
if done.returncode != 0:
|
|
242
|
+
raise Stop('%s failed: %s' % (command[0], (done.stderr or '').strip().splitlines()[-1:] or 'no output'))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def duration(path):
|
|
246
|
+
try:
|
|
247
|
+
done = subprocess.run(['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', path],
|
|
248
|
+
capture_output=True, text=True, timeout=60)
|
|
249
|
+
return float(done.stdout.strip())
|
|
250
|
+
except Exception:
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def mux(video, mix_wav, out, length, force):
|
|
255
|
+
ensure_free([out, track_of(out)], force)
|
|
256
|
+
part = os.path.join(os.path.dirname(os.path.abspath(out)), '.%s.part' % os.path.basename(out))
|
|
257
|
+
run(['ffmpeg', '-y', '-loglevel', 'error', '-i', video, '-i', mix_wav, '-map', '0:v:0', '-map', '1:a:0',
|
|
258
|
+
'-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k', '-movflags', '+faststart', '-t', '%.3f' % length, '-f', 'mp4', part])
|
|
259
|
+
os.replace(part, out)
|
|
260
|
+
track = track_of(out)
|
|
261
|
+
run(['ffmpeg', '-y', '-loglevel', 'error', '-i', mix_wav, '-c:a', 'libmp3lame', '-b:a', '192k', track])
|
|
262
|
+
poster = os.path.splitext(video)[0] + '-poster.jpg'
|
|
263
|
+
new_poster = os.path.splitext(out)[0] + '-poster.jpg'
|
|
264
|
+
if os.path.exists(poster) and (force or not os.path.exists(new_poster)):
|
|
265
|
+
shutil.copyfile(poster, new_poster)
|
|
266
|
+
os.remove(mix_wav)
|
|
267
|
+
return track, new_poster if os.path.exists(new_poster) else None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ---- the script
|
|
271
|
+
|
|
272
|
+
def clean(line):
|
|
273
|
+
text = re.sub(r'\*\(optional\)\*', '', line)
|
|
274
|
+
text = re.sub(r'\[([^\]]+)\]\([^)]*\)', r'\1', text)
|
|
275
|
+
return ' '.join(re.sub(r'[*`]', '', text).split())
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def load_sections(path, drop_optional=False):
|
|
279
|
+
try:
|
|
280
|
+
with open(path, encoding='utf-8') as handle:
|
|
281
|
+
lines = handle.read().splitlines()
|
|
282
|
+
except OSError as error:
|
|
283
|
+
raise Stop('Cannot read the script: %s' % error)
|
|
284
|
+
sections, current = [], None
|
|
285
|
+
for line in lines:
|
|
286
|
+
found = HEADING.match(line)
|
|
287
|
+
if found:
|
|
288
|
+
current = {'id': found.group(1), 'title': found.group(2), 'start': seconds_of(found.group(3)),
|
|
289
|
+
'end': seconds_of(found.group(4)), 'spoken': [], 'optional': []}
|
|
290
|
+
sections.append(current)
|
|
291
|
+
elif line.startswith('## '):
|
|
292
|
+
current = None
|
|
293
|
+
elif current is not None and line.startswith('>'):
|
|
294
|
+
spoken = clean(line.lstrip('>'))
|
|
295
|
+
if spoken:
|
|
296
|
+
(current['optional'] if '*(optional)*' in line else current['spoken']).append((len(current['spoken']) + len(current['optional']), spoken))
|
|
297
|
+
if not sections:
|
|
298
|
+
raise Stop('No timed sections found. Each section needs a heading like "## D01 Opening (00:00.0 to 00:21.4)" and its '
|
|
299
|
+
'spoken lines as "> " quotes. See the elevenlabs-voiceover skill.')
|
|
300
|
+
result, previous_end = [], 0.0
|
|
301
|
+
for section in sections:
|
|
302
|
+
if section['end'] <= section['start']:
|
|
303
|
+
raise Stop('Section %s ends before it starts.' % section['id'])
|
|
304
|
+
if section['start'] < previous_end - SLACK:
|
|
305
|
+
raise Stop('Section %s starts before the previous section ends.' % section['id'])
|
|
306
|
+
previous_end = section['end']
|
|
307
|
+
kept = sorted(section['spoken'] + ([] if drop_optional else section['optional']))
|
|
308
|
+
text = ' '.join(item[1] for item in kept)
|
|
309
|
+
if not text:
|
|
310
|
+
continue
|
|
311
|
+
result.append({
|
|
312
|
+
'id': section['id'], 'title': section['title'], 'start': section['start'], 'end': section['end'],
|
|
313
|
+
'window': round(section['end'] - section['start'], 3), 'text': text, 'chars': len(text),
|
|
314
|
+
'optional_chars': sum(len(item[1]) for item in section['optional']),
|
|
315
|
+
})
|
|
316
|
+
if not result:
|
|
317
|
+
raise Stop('The script has sections but no spoken lines ("> " quotes).')
|
|
318
|
+
return result
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def check_inputs(args):
|
|
322
|
+
for attribute in ('script', 'video', 'audio'):
|
|
323
|
+
value = getattr(args, attribute, None)
|
|
324
|
+
if value and not os.path.isfile(value):
|
|
325
|
+
raise Stop('%s not found: %s' % (attribute.capitalize(), value), USAGE)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def work_dir(args):
|
|
329
|
+
return args.work or os.path.join(os.path.dirname(os.path.abspath(args.out)), stem_of(args.out) + '-voiceover-sections')
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def audio_ext(fmt):
|
|
333
|
+
if not re.fullmatch(r'(mp3|wav)_\d+(_\d+)?', fmt):
|
|
334
|
+
raise Stop('The format must be an mp3_* or wav_* one, for example %s.' % DEFAULT_FORMAT, USAGE)
|
|
335
|
+
return fmt.split('_')[0]
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# ---- commands
|
|
339
|
+
|
|
340
|
+
def cmd_sections(args):
|
|
341
|
+
check_inputs(args)
|
|
342
|
+
sections = load_sections(args.script, args.drop_optional)
|
|
343
|
+
lines = ['%-5s %-34s %-18s %7s %6s %9s' % ('id', 'section', 'window', 'seconds', 'chars', 'optional')]
|
|
344
|
+
for s in sections:
|
|
345
|
+
lines.append('%-5s %-34s %-18s %7.1f %6d %9d' % (s['id'], s['title'][:34], '%s to %s' % (mmss(s['start']), mmss(s['end'])),
|
|
346
|
+
s['window'], s['chars'], s['optional_chars']))
|
|
347
|
+
lines.append('%d sections, %d characters.' % (len(sections), sum(s['chars'] for s in sections)))
|
|
348
|
+
emit(args, {'sections': sections, 'characters': sum(s['chars'] for s in sections)}, lines)
|
|
349
|
+
return OK
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def cmd_voices(args):
|
|
353
|
+
query = {'page_size': str(args.limit)}
|
|
354
|
+
if args.search:
|
|
355
|
+
query['search'] = args.search
|
|
356
|
+
status, _, body = call('GET', '/v2/voices', query=query)
|
|
357
|
+
if status != 200:
|
|
358
|
+
raise Stop('Could not list voices. ' + explain(status, body))
|
|
359
|
+
voices = json.loads(body).get('voices', [])
|
|
360
|
+
rows = [{'id': v.get('voice_id'), 'name': v.get('name'), 'category': v.get('category'), 'labels': v.get('labels') or {}} for v in voices]
|
|
361
|
+
lines = ['%-24s %-22s %-10s %s' % ('id', 'name', 'category', 'labels')]
|
|
362
|
+
for r in rows:
|
|
363
|
+
lines.append('%-24s %-22s %-10s %s' % (r['id'], (r['name'] or '')[:22], r['category'] or '', ', '.join('%s=%s' % kv for kv in r['labels'].items())))
|
|
364
|
+
emit(args, {'voices': rows}, lines)
|
|
365
|
+
return OK
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def cmd_account(args):
|
|
369
|
+
found, why = account()
|
|
370
|
+
if not found:
|
|
371
|
+
raise Stop('The key works for requests but the account cannot be read. %s Grant the key "User: read" to see credits.' % why)
|
|
372
|
+
resets = datetime.datetime.fromtimestamp(found['resets'], datetime.timezone.utc).strftime('%Y-%m-%d') if found['resets'] else 'unknown'
|
|
373
|
+
emit(args, found, ['Plan: %s (%s)' % (found['tier'], found['status']), 'Characters used: %s of %s' % (found['used'], found['limit']),
|
|
374
|
+
'Remaining: %s' % found['remaining'], 'Resets: %s' % resets])
|
|
375
|
+
return OK
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def cmd_speak(args):
|
|
379
|
+
try:
|
|
380
|
+
with open(args.text_file, encoding='utf-8') as handle:
|
|
381
|
+
text = handle.read().strip()
|
|
382
|
+
except OSError:
|
|
383
|
+
text = ''
|
|
384
|
+
if not text:
|
|
385
|
+
raise Stop('The text file is missing or empty: %s' % args.text_file, USAGE)
|
|
386
|
+
key()
|
|
387
|
+
remaining = remaining_characters()
|
|
388
|
+
plan = {'voice': args.voice, 'model': args.model, 'format': args.format, 'characters': len(text), 'remaining': remaining, 'out': args.out}
|
|
389
|
+
if not args.confirm:
|
|
390
|
+
emit(args, dict(plan, confirmed=False), ['Plan only, nothing was sent.', 'Voice %s, model %s, %d characters.' % (args.voice, args.model, len(text)),
|
|
391
|
+
'Credits left: %s.' % ('unknown' if remaining is None else remaining), 'Run again with --confirm to create %s.' % args.out])
|
|
392
|
+
return PLAN_ONLY
|
|
393
|
+
ensure_free([args.out], args.force)
|
|
394
|
+
data, meta = synthesize(text, args.voice, args.model, args.format)
|
|
395
|
+
write_atomic(args.out, data, args.force)
|
|
396
|
+
emit(args, dict(plan, confirmed=True, bytes=len(data), seconds=duration(args.out), **meta), ['Wrote %s (%d bytes).' % (args.out, len(data))])
|
|
397
|
+
return OK
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def cmd_isolate(args):
|
|
401
|
+
check_inputs(args)
|
|
402
|
+
key()
|
|
403
|
+
seconds = duration(args.audio)
|
|
404
|
+
plan = {'audio': args.audio, 'seconds': seconds, 'out': args.out}
|
|
405
|
+
if not args.confirm:
|
|
406
|
+
emit(args, dict(plan, confirmed=False), ['Plan only, nothing was sent.', 'Would clean %s (%s seconds) and write %s.' % (args.audio, seconds, args.out),
|
|
407
|
+
'Run again with --confirm.'])
|
|
408
|
+
return PLAN_ONLY
|
|
409
|
+
ensure_free([args.out], args.force)
|
|
410
|
+
body, content_type = multipart({}, [('audio', os.path.basename(args.audio), read_bytes(args.audio))])
|
|
411
|
+
status, _, data = call('POST', '/v1/audio-isolation', body=body, content_type=content_type)
|
|
412
|
+
if status != 200 or not data:
|
|
413
|
+
raise Stop('Isolation failed. ' + explain(status, data))
|
|
414
|
+
write_atomic(args.out, data, args.force)
|
|
415
|
+
emit(args, dict(plan, confirmed=True, bytes=len(data)), ['Wrote %s (%d bytes).' % (args.out, len(data))])
|
|
416
|
+
return OK
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def cmd_voiceover(args):
|
|
420
|
+
check_inputs(args)
|
|
421
|
+
need_tools()
|
|
422
|
+
key()
|
|
423
|
+
ext = audio_ext(args.format)
|
|
424
|
+
sections = load_sections(args.script, args.drop_optional)
|
|
425
|
+
length = duration(args.video)
|
|
426
|
+
if not length:
|
|
427
|
+
raise Stop('Cannot read the video duration: %s' % args.video)
|
|
428
|
+
if sections[-1]['end'] > length + 1.0:
|
|
429
|
+
raise Stop('The script runs to %s but the video is only %s long. Is this the right draft?' % (mmss(sections[-1]['end']), mmss(length)))
|
|
430
|
+
work = work_dir(args)
|
|
431
|
+
for s in sections:
|
|
432
|
+
s['fingerprint'] = sha(s['text'], args.voice, args.model, args.format, pronunciations())
|
|
433
|
+
s['file'] = os.path.join(work, '%s.%s' % (safe_id(s['id']), ext))
|
|
434
|
+
meta_path = os.path.join(work, '%s.json' % safe_id(s['id']))
|
|
435
|
+
try:
|
|
436
|
+
s['cached'] = os.path.isfile(s['file']) and not args.redo and read_json(meta_path)['fingerprint'] == s['fingerprint']
|
|
437
|
+
except (OSError, ValueError, KeyError):
|
|
438
|
+
s['cached'] = False
|
|
439
|
+
todo = [s for s in sections if not s['cached']]
|
|
440
|
+
to_send = sum(s['chars'] for s in todo)
|
|
441
|
+
remaining = remaining_characters()
|
|
442
|
+
rows = [dict(id=s['id'], title=s['title'], window=s['window'], chars=s['chars'], rough_seconds=round(s['chars'] / SPEECH_CHARS_PER_SECOND, 1),
|
|
443
|
+
may_be_long=s['chars'] / SPEECH_CHARS_PER_SECOND > s['window'] * 1.05, cached=s['cached'], has_optional=s['optional_chars'] > 0)
|
|
444
|
+
for s in sections]
|
|
445
|
+
plan = {'script': args.script, 'video': args.video, 'out': args.out, 'voice': args.voice, 'model': args.model, 'sections': rows,
|
|
446
|
+
'characters_to_send': to_send, 'remaining': remaining, 'enough': None if remaining is None else remaining >= to_send, 'work': work}
|
|
447
|
+
if not args.confirm:
|
|
448
|
+
lines = ['Plan only, nothing was sent.', 'Voice %s, model %s. Video %s long.' % (args.voice, args.model, mmss(length)),
|
|
449
|
+
'%-5s %-30s %7s %6s %9s %s' % ('id', 'section', 'window', 'chars', '~seconds', 'note')]
|
|
450
|
+
for r in rows:
|
|
451
|
+
note = 'already made, no cost' if r['cached'] else ('may run long' + (' (has an optional sentence)' if r['has_optional'] else '') if r['may_be_long'] else '')
|
|
452
|
+
lines.append('%-5s %-30s %6.1fs %6d %9.1f %s' % (r['id'], r['title'][:30], r['window'], r['chars'], r['rough_seconds'], note))
|
|
453
|
+
lines += ['%d characters would be sent; credits left: %s.' % (to_send, 'unknown' if remaining is None else remaining),
|
|
454
|
+
'"~seconds" is a rough estimate from the character count. Run again with --confirm to create the voiceover.']
|
|
455
|
+
emit(args, dict(plan, confirmed=False), lines)
|
|
456
|
+
return PLAN_ONLY
|
|
457
|
+
if remaining is not None and remaining < to_send:
|
|
458
|
+
raise Stop('Not enough credits: %d characters to send, %d left. Nothing was sent.' % (to_send, remaining))
|
|
459
|
+
ensure_free([args.out, track_of(args.out)], args.force)
|
|
460
|
+
os.makedirs(work, exist_ok=True)
|
|
461
|
+
for s in todo:
|
|
462
|
+
data, meta = synthesize(s['text'], args.voice, args.model, args.format)
|
|
463
|
+
write_atomic(s['file'], data, True)
|
|
464
|
+
with open(os.path.join(work, '%s.json' % safe_id(s['id'])), 'w') as handle:
|
|
465
|
+
json.dump(dict(meta, fingerprint=s['fingerprint'], id=s['id']), handle)
|
|
466
|
+
report = []
|
|
467
|
+
for s in sections:
|
|
468
|
+
seconds = duration(s['file'])
|
|
469
|
+
row = {'id': s['id'], 'title': s['title'], 'window': s['window'], 'audio_seconds': seconds, 'fit': 'ok', 'speedup': 1.0, 'audio': s['file']}
|
|
470
|
+
if seconds is None:
|
|
471
|
+
raise Stop('Cannot read the audio for section %s.' % s['id'])
|
|
472
|
+
if seconds > s['window'] + SLACK:
|
|
473
|
+
ratio = seconds / s['window']
|
|
474
|
+
if ratio <= args.max_speedup:
|
|
475
|
+
fitted = os.path.join(work, '%s-fit.wav' % safe_id(s['id']))
|
|
476
|
+
run(['ffmpeg', '-y', '-loglevel', 'error', '-i', s['file'], '-filter:a', 'atempo=%.5f' % ratio, '-ar', '44100', '-ac', '1', fitted])
|
|
477
|
+
row.update(fit='sped_up', speedup=round(ratio, 4), audio=fitted, audio_seconds=duration(fitted))
|
|
478
|
+
else:
|
|
479
|
+
row.update(fit='too_long', speedup=round(ratio, 4), over_by=round(seconds - s['window'], 2), has_optional=s['optional_chars'] > 0)
|
|
480
|
+
report.append(row)
|
|
481
|
+
too_long = [r for r in report if r['fit'] == 'too_long']
|
|
482
|
+
if too_long:
|
|
483
|
+
lines = ['Created %d new section(s), but %d %s not fit its window, so no video was made. Finished sections are kept.' % (len(todo), len(too_long), 'does' if len(too_long) == 1 else 'do')]
|
|
484
|
+
lines += [' %s %s: %.1fs of speech in a %.1fs window (%.1fs over)%s' % (r['id'], r['title'], r['audio_seconds'], r['window'], r['over_by'],
|
|
485
|
+
', has an optional sentence to drop' if r['has_optional'] else '') for r in too_long]
|
|
486
|
+
lines.append('Shorten the text in the script (or use --drop-optional), then run again: only the changed sections are sent.')
|
|
487
|
+
emit(args, {'confirmed': True, 'fit': False, 'sections': report}, lines)
|
|
488
|
+
return NO_FIT
|
|
489
|
+
mix = os.path.join(work, '_mix.wav')
|
|
490
|
+
chains = ['[%d:a]aformat=sample_rates=44100:channel_layouts=mono,adelay=%d:all=1[a%d]' % (i + 1, round(s['start'] * 1000), i) for i, s in enumerate(sections)]
|
|
491
|
+
chains.append('%samix=inputs=%d:duration=longest:normalize=0,apad=whole_dur=%.3f,atrim=end=%.3f[aout]'
|
|
492
|
+
% (''.join('[a%d]' % i for i in range(len(sections))), len(sections), length, length))
|
|
493
|
+
run(['ffmpeg', '-y', '-loglevel', 'error', '-i', args.video] + [x for r in report for x in ('-i', r['audio'])] +
|
|
494
|
+
['-filter_complex', ';'.join(chains), '-map', '[aout]', '-ar', '44100', '-ac', '2', mix])
|
|
495
|
+
track, poster = mux(args.video, mix, args.out, length, args.force)
|
|
496
|
+
summary = {'created_utc': datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'), 'script': args.script, 'video': args.video,
|
|
497
|
+
'voice': args.voice, 'model': args.model, 'format': args.format, 'characters_sent': to_send, 'sections': report}
|
|
498
|
+
with open(os.path.splitext(args.out)[0] + '-voiceover.json', 'w') as handle:
|
|
499
|
+
json.dump(summary, handle, indent=2)
|
|
500
|
+
emit(args, dict(summary, confirmed=True, fit=True, out=args.out, track=track, poster=poster),
|
|
501
|
+
['Wrote %s' % args.out, 'Voiceover track: %s' % track] + (['Poster: %s' % poster] if poster else []) +
|
|
502
|
+
['%d sections placed, %d sped up (never more than %.0f%%), %d characters sent.' % (len(report), sum(r['fit'] == 'sped_up' for r in report),
|
|
503
|
+
(args.max_speedup - 1) * 100, to_send),
|
|
504
|
+
'Sections kept in %s so a re-run only sends what changed. Remove that folder when the video is final.' % work])
|
|
505
|
+
return OK
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def cmd_polish(args):
|
|
509
|
+
check_inputs(args)
|
|
510
|
+
need_tools()
|
|
511
|
+
length, take = duration(args.video), duration(args.audio)
|
|
512
|
+
if not length or not take:
|
|
513
|
+
raise Stop('Cannot read the duration of the video or the recording.')
|
|
514
|
+
if args.isolate:
|
|
515
|
+
key()
|
|
516
|
+
work = work_dir(args)
|
|
517
|
+
plan = {'audio': args.audio, 'take_seconds': take, 'video': args.video, 'video_seconds': length, 'out': args.out, 'isolate': args.isolate,
|
|
518
|
+
'normalize': args.normalize, 'trim': take > length + 0.5, 'pad': take < length - 0.5}
|
|
519
|
+
if args.isolate and not args.confirm:
|
|
520
|
+
emit(args, dict(plan, confirmed=False), ['Plan only, nothing was sent.', 'The take (%.1fs) would be cleaned by ElevenLabs, then levelled and placed on the %.1fs video.' % (take, length),
|
|
521
|
+
'Run again with --confirm.'])
|
|
522
|
+
return PLAN_ONLY
|
|
523
|
+
ensure_free([args.out, track_of(args.out)], args.force)
|
|
524
|
+
os.makedirs(work, exist_ok=True)
|
|
525
|
+
source = args.audio
|
|
526
|
+
if args.isolate:
|
|
527
|
+
cleaned = os.path.join(work, 'take-clean-%s.mp3' % hashlib.sha256(read_bytes(args.audio)).hexdigest()[:12])
|
|
528
|
+
if not os.path.isfile(cleaned):
|
|
529
|
+
body, content_type = multipart({}, [('audio', os.path.basename(args.audio), read_bytes(args.audio))])
|
|
530
|
+
status, _, data = call('POST', '/v1/audio-isolation', body=body, content_type=content_type)
|
|
531
|
+
if status != 200 or not data:
|
|
532
|
+
raise Stop('Isolation failed. ' + explain(status, data))
|
|
533
|
+
write_atomic(cleaned, data, True)
|
|
534
|
+
source = cleaned
|
|
535
|
+
filters = (['loudnorm=I=-16:TP=-1.5:LRA=11'] if args.normalize else []) + (['afade=t=out:st=%.3f:d=0.3' % (length - 0.3)] if take > length + 0.5 else [])
|
|
536
|
+
filters += ['apad=whole_dur=%.3f' % length, 'atrim=end=%.3f' % length]
|
|
537
|
+
mix = os.path.join(work, '_mix.wav')
|
|
538
|
+
run(['ffmpeg', '-y', '-loglevel', 'error', '-i', source, '-af', ','.join(filters), '-ar', '44100', '-ac', '2', mix])
|
|
539
|
+
track, poster = mux(args.video, mix, args.out, length, args.force)
|
|
540
|
+
emit(args, dict(plan, confirmed=True, out=args.out, track=track, poster=poster),
|
|
541
|
+
['Wrote %s' % args.out, 'Voiceover track: %s' % track] + (['Trimmed the take to the video length.'] if plan['trim'] else []))
|
|
542
|
+
return OK
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
# ---- command line
|
|
546
|
+
|
|
547
|
+
def text_arg(value):
|
|
548
|
+
if re.search(r'["$`\\\n]', value):
|
|
549
|
+
raise argparse.ArgumentTypeError('may not contain quotes, $, backticks or backslashes')
|
|
550
|
+
return value
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def build_parser():
|
|
554
|
+
parser = argparse.ArgumentParser(prog='elevenlabs.py', description='ElevenLabs for the final voiceover of a narrated video.')
|
|
555
|
+
sub = parser.add_subparsers(dest='command', required=True)
|
|
556
|
+
|
|
557
|
+
def common(p, voice=False, confirm=False, out=False, work=False):
|
|
558
|
+
p.add_argument('--json', action='store_true', help='print JSON')
|
|
559
|
+
if voice:
|
|
560
|
+
p.add_argument('--voice', type=text_arg, default=os.environ.get('ELEVENLABS_VOICE', ''), help='voice id')
|
|
561
|
+
p.add_argument('--model', type=text_arg, default=os.environ.get('ELEVENLABS_MODEL') or DEFAULT_MODEL)
|
|
562
|
+
p.add_argument('--format', type=text_arg, default=DEFAULT_FORMAT)
|
|
563
|
+
if confirm:
|
|
564
|
+
p.add_argument('--confirm', action='store_true', help='actually send; without it only the plan is printed')
|
|
565
|
+
if out:
|
|
566
|
+
p.add_argument('--out', type=text_arg, required=True)
|
|
567
|
+
p.add_argument('--force', action='store_true', help='replace an existing file')
|
|
568
|
+
if work:
|
|
569
|
+
p.add_argument('--work', type=text_arg, default='', help='where the per-section audio is kept')
|
|
570
|
+
|
|
571
|
+
p = sub.add_parser('sections', help='the timed sections of a voiceover script')
|
|
572
|
+
p.add_argument('--script', type=text_arg, required=True)
|
|
573
|
+
p.add_argument('--drop-optional', action='store_true')
|
|
574
|
+
common(p)
|
|
575
|
+
p = sub.add_parser('voices', help='list the voices on the account')
|
|
576
|
+
p.add_argument('--search', type=text_arg, default='')
|
|
577
|
+
p.add_argument('--limit', type=int, default=30)
|
|
578
|
+
common(p)
|
|
579
|
+
p = sub.add_parser('account', help='plan and characters left')
|
|
580
|
+
common(p)
|
|
581
|
+
p = sub.add_parser('speak', help='one text file to one audio file')
|
|
582
|
+
p.add_argument('--text-file', type=text_arg, required=True)
|
|
583
|
+
common(p, voice=True, confirm=True, out=True)
|
|
584
|
+
p = sub.add_parser('isolate', help='remove background noise from a recording')
|
|
585
|
+
p.add_argument('--audio', type=text_arg, required=True)
|
|
586
|
+
common(p, confirm=True, out=True)
|
|
587
|
+
p = sub.add_parser('voiceover', help='speech for every section, fitted to its window, on the approved draft')
|
|
588
|
+
p.add_argument('--script', type=text_arg, required=True)
|
|
589
|
+
p.add_argument('--video', type=text_arg, required=True)
|
|
590
|
+
p.add_argument('--drop-optional', action='store_true')
|
|
591
|
+
p.add_argument('--max-speedup', type=float, default=1.08)
|
|
592
|
+
p.add_argument('--redo', action='store_true', help='ignore the sections already made')
|
|
593
|
+
common(p, voice=True, confirm=True, out=True, work=True)
|
|
594
|
+
p = sub.add_parser('polish', help='level a recorded take and put it on the approved draft')
|
|
595
|
+
p.add_argument('--audio', type=text_arg, required=True)
|
|
596
|
+
p.add_argument('--video', type=text_arg, required=True)
|
|
597
|
+
p.add_argument('--isolate', action='store_true', help='clean the take with ElevenLabs first')
|
|
598
|
+
p.add_argument('--no-normalize', dest='normalize', action='store_false')
|
|
599
|
+
common(p, confirm=True, out=True, work=True)
|
|
600
|
+
return parser
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
COMMANDS = {'sections': cmd_sections, 'voices': cmd_voices, 'account': cmd_account, 'speak': cmd_speak, 'isolate': cmd_isolate,
|
|
604
|
+
'voiceover': cmd_voiceover, 'polish': cmd_polish}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def main(argv):
|
|
608
|
+
args = build_parser().parse_args(argv)
|
|
609
|
+
if args.command in ('speak', 'voiceover'):
|
|
610
|
+
if not args.voice:
|
|
611
|
+
print('No voice. Pass --voice <id>, or set voice_id in settings.yml (list them: -a voices).', file=sys.stderr)
|
|
612
|
+
return USAGE
|
|
613
|
+
audio_ext(args.format)
|
|
614
|
+
try:
|
|
615
|
+
if args.command in ('speak', 'voiceover'):
|
|
616
|
+
args.voice = resolve_voice(args.voice)
|
|
617
|
+
return COMMANDS[args.command](args)
|
|
618
|
+
except Stop as stop:
|
|
619
|
+
print(stop, file=sys.stderr)
|
|
620
|
+
return stop.code
|
|
621
|
+
except KeyboardInterrupt:
|
|
622
|
+
print('Interrupted. Sections already made are kept.', file=sys.stderr)
|
|
623
|
+
return ERROR
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
if __name__ == '__main__':
|
|
627
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# 1. Create ProgressBar function
|
|
4
|
+
# 1.1 Input is currentState($1) and totalState($2)
|
|
5
|
+
function progress_bar {
|
|
6
|
+
# Process data
|
|
7
|
+
let _progress=(${1}*100/${2}*100)/100
|
|
8
|
+
let _done=(${_progress}*4)/10
|
|
9
|
+
let _left=40-$_done
|
|
10
|
+
# Build progressbar string lengths
|
|
11
|
+
_fill=$(printf "%${_done}s")
|
|
12
|
+
_empty=$(printf "%${_left}s")
|
|
13
|
+
|
|
14
|
+
# 1.2 Build progressbar strings and print the ProgressBar line
|
|
15
|
+
# 1.2.1 Output example:
|
|
16
|
+
# 1.2.1.1 Progress : [########################################] 100%
|
|
17
|
+
printf "\rProgress : [${_fill// /\#}${_empty// /-}] ${_progress}%%\n"
|
|
18
|
+
|
|
19
|
+
}
|