@1agh/maude 0.42.0 → 0.44.0
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/apps/studio/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/acp/bridge.ts +44 -10
- package/apps/studio/acp/env.ts +23 -0
- package/apps/studio/acp/login-state.ts +263 -0
- package/apps/studio/acp/plugin-bootstrap.ts +31 -25
- package/apps/studio/acp/probe.ts +152 -1
- package/apps/studio/annotations-layer.tsx +1451 -324
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +568 -24
- package/apps/studio/bin/_audio-search.mjs +140 -0
- package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
- package/apps/studio/bin/_import-asset.mjs +815 -0
- package/apps/studio/bin/_import-brand.mjs +635 -0
- package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
- package/apps/studio/bin/_import-tokens.mjs +1201 -0
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/audio-search.sh +28 -0
- package/apps/studio/bin/generate.sh +154 -0
- package/apps/studio/bin/import-asset.sh +34 -0
- package/apps/studio/bin/import-brand.sh +29 -0
- package/apps/studio/bin/import-tokens.sh +33 -0
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +258 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +656 -12
- package/apps/studio/canvas-lib.tsx +637 -10
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +2385 -699
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +18 -0
- package/apps/studio/client/inspector-controls.jsx +781 -0
- package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
- package/apps/studio/client/panels/ChatPanel.jsx +15 -33
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
- package/apps/studio/client/panels/ReadinessList.jsx +227 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +432 -0
- package/apps/studio/client/styles/3-shell-maude.css +376 -28
- package/apps/studio/client/styles/4-components.css +223 -0
- package/apps/studio/client/styles/6-acp-chat.css +86 -0
- package/apps/studio/client/tour/quick-setup-tour.js +39 -0
- package/apps/studio/commands/annotation-strokes-command.ts +13 -3
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +42 -8
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +5572 -21
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/draw/palette.ts +34 -0
- package/apps/studio/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +287 -0
- package/apps/studio/footage/schema.ts +662 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
- package/apps/studio/generation/adapters/gemini.ts +542 -0
- package/apps/studio/generation/adapters/groq.test.ts +80 -0
- package/apps/studio/generation/adapters/groq.ts +247 -0
- package/apps/studio/generation/audio-library.test.ts +95 -0
- package/apps/studio/generation/audio-library.ts +156 -0
- package/apps/studio/generation/captions.test.ts +78 -0
- package/apps/studio/generation/captions.ts +132 -0
- package/apps/studio/generation/download.ts +76 -0
- package/apps/studio/generation/elevenlabs.test.ts +223 -0
- package/apps/studio/generation/gemini.test.ts +350 -0
- package/apps/studio/generation/jobs.test.ts +128 -0
- package/apps/studio/generation/jobs.ts +243 -0
- package/apps/studio/generation/keys.test.ts +75 -0
- package/apps/studio/generation/keys.ts +162 -0
- package/apps/studio/generation/prefs.test.ts +95 -0
- package/apps/studio/generation/prefs.ts +82 -0
- package/apps/studio/generation/registry.ts +88 -0
- package/apps/studio/generation/types.test.ts +123 -0
- package/apps/studio/generation/types.ts +266 -0
- package/apps/studio/generation/whisper-models.test.ts +152 -0
- package/apps/studio/generation/whisper-models.ts +257 -0
- package/apps/studio/generation/whisper-spike-results.md +61 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +199 -21
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +1039 -22
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +20 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/readiness.ts +108 -20
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +18 -2
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/_helpers.ts +15 -2
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-bridge.test.ts +34 -1
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/acp-env.test.ts +30 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
- package/apps/studio/test/acp-session-plugins.test.ts +72 -1
- package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
- package/apps/studio/test/annotations-layer.test.ts +174 -0
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +50 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
- package/apps/studio/test/csrf-write-guard.test.ts +23 -1
- package/apps/studio/test/design-setup-readiness.test.ts +155 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +154 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/generate-route.test.ts +106 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/import-asset-browser.test.ts +64 -0
- package/apps/studio/test/import-asset-route.test.ts +71 -0
- package/apps/studio/test/import-asset.test.ts +399 -0
- package/apps/studio/test/import-brand-browser.test.ts +88 -0
- package/apps/studio/test/import-brand-route.test.ts +120 -0
- package/apps/studio/test/import-brand.test.ts +206 -0
- package/apps/studio/test/import-tokens.test.ts +722 -0
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/tour-overlay.test.tsx +16 -0
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +49 -21
- package/apps/studio/ui-prefs.ts +130 -0
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +40 -1
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +149 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +67 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/cli/lib/pkg-root.mjs +107 -0
- package/cli/lib/pkg-root.test.mjs +79 -0
- package/cli/lib/reconstruct-toolset.test.mjs +194 -0
- package/cli/lib/update-check.mjs +8 -0
- package/package.json +13 -11
- package/plugins/design/dependencies.json +18 -0
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// generation/whisper-models.ts — managed local whisper.cpp GGML models (Task 2.7,
|
|
2
|
+
// approach A, DDR-164). The "one-click local subtitles" story WITHOUT a heavy
|
|
3
|
+
// WASM dependency: keep the fast native whisper.cpp engine, but remove the
|
|
4
|
+
// hand-fetch-a-ggml-from-Hugging-Face friction the owner hit in testing — a
|
|
5
|
+
// Settings button downloads a model into a Maude-managed, gitignored cache and
|
|
6
|
+
// `maude design transcribe --provider whisper` auto-resolves it (no --model).
|
|
7
|
+
//
|
|
8
|
+
// The whisper.cpp BINARY is still a soft dep (brew / build); this closes the
|
|
9
|
+
// MODEL half of the friction. Models live OUTSIDE any served project tree (a
|
|
10
|
+
// per-machine cache, re-downloadable), never in `.design/`, never committed.
|
|
11
|
+
//
|
|
12
|
+
// This module owns the model REGISTRY + dir + list/resolve (fs, server-side).
|
|
13
|
+
// The actual download (streamed, SSRF-hardened, progress-tracked) lives in the
|
|
14
|
+
// http route so the egress discipline sits next to the other provider egress.
|
|
15
|
+
|
|
16
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
17
|
+
import { mkdir, rename, rm } from 'node:fs/promises';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
export interface WhisperModelDescriptor {
|
|
22
|
+
/** Stable id used by the config + route (`base`, `base.en`, …). */
|
|
23
|
+
id: string;
|
|
24
|
+
/** The on-disk ggml filename. */
|
|
25
|
+
file: string;
|
|
26
|
+
label: string;
|
|
27
|
+
/** Approximate download size, for the consent copy + the download size cap. */
|
|
28
|
+
sizeMB: number;
|
|
29
|
+
/** Multilingual vs English-only (the owner's Czech-footage gotcha: `.en`
|
|
30
|
+
* garbles non-English — surface this in the picker). */
|
|
31
|
+
multilingual: boolean;
|
|
32
|
+
note: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The subset of ggerganov/whisper.cpp ggml models Maude offers. Kept small +
|
|
36
|
+
// curated (a fixed allowlist — the download URL is derived from `file`, never
|
|
37
|
+
// user input, so this is also the SSRF allowlist for the model host).
|
|
38
|
+
export const WHISPER_MODELS: readonly WhisperModelDescriptor[] = [
|
|
39
|
+
{
|
|
40
|
+
id: 'tiny',
|
|
41
|
+
file: 'ggml-tiny.bin',
|
|
42
|
+
label: 'Tiny (multilingual)',
|
|
43
|
+
sizeMB: 75,
|
|
44
|
+
multilingual: true,
|
|
45
|
+
note: 'Fastest, lowest accuracy. Good for a quick draft in any language.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'base',
|
|
49
|
+
file: 'ggml-base.bin',
|
|
50
|
+
label: 'Base (multilingual)',
|
|
51
|
+
sizeMB: 142,
|
|
52
|
+
multilingual: true,
|
|
53
|
+
note: 'The recommended default — works in any language (incl. Czech). Modest accuracy.',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'base.en',
|
|
57
|
+
file: 'ggml-base.en.bin',
|
|
58
|
+
label: 'Base (English-only)',
|
|
59
|
+
sizeMB: 142,
|
|
60
|
+
multilingual: false,
|
|
61
|
+
note: 'English audio only — do NOT use for other languages (it garbles them).',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'small',
|
|
65
|
+
file: 'ggml-small.bin',
|
|
66
|
+
label: 'Small (multilingual)',
|
|
67
|
+
sizeMB: 466,
|
|
68
|
+
multilingual: true,
|
|
69
|
+
note: 'Noticeably more accurate than base, ~3× larger.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'large-v3-turbo',
|
|
73
|
+
file: 'ggml-large-v3-turbo.bin',
|
|
74
|
+
label: 'Large v3 Turbo (multilingual)',
|
|
75
|
+
sizeMB: 1560,
|
|
76
|
+
multilingual: true,
|
|
77
|
+
note: 'Best accuracy, ~1.5 GB. Slower and disk-heavy, but close to cloud quality.',
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const MODEL_HOST = 'https://huggingface.co';
|
|
82
|
+
const MODEL_REPO_PATH = '/ggerganov/whisper.cpp/resolve/main';
|
|
83
|
+
|
|
84
|
+
/** The fixed, non-interpolated download URL for a model (SSRF-safe — the file
|
|
85
|
+
* comes from the frozen registry above, never from a request). */
|
|
86
|
+
export function whisperModelUrl(m: WhisperModelDescriptor): string {
|
|
87
|
+
return `${MODEL_HOST}${MODEL_REPO_PATH}/${m.file}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getWhisperModel(id: unknown): WhisperModelDescriptor | null {
|
|
91
|
+
return WHISPER_MODELS.find((m) => m.id === id) ?? null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Maude-managed model cache dir — per-machine, gitignored, never in a project.
|
|
95
|
+
* XDG_CACHE_HOME-aware (falls back to ~/.cache), mirroring keys.ts's XDG logic
|
|
96
|
+
* but a CACHE location (large, re-downloadable) not a config one. */
|
|
97
|
+
export function whisperModelsDir(): string {
|
|
98
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
99
|
+
const base = xdg && xdg.length > 0 ? xdg : join(homedir(), '.cache');
|
|
100
|
+
return join(base, 'maude', 'whisper-models');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface WhisperModelStatus extends WhisperModelDescriptor {
|
|
104
|
+
downloaded: boolean;
|
|
105
|
+
/** Absolute path when downloaded. */
|
|
106
|
+
path?: string;
|
|
107
|
+
/** Actual on-disk bytes when downloaded (for the "Remove" affordance). */
|
|
108
|
+
bytes?: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The absolute on-disk path a model WOULD live at (whether present or not). */
|
|
112
|
+
export function whisperModelPath(m: WhisperModelDescriptor): string {
|
|
113
|
+
return join(whisperModelsDir(), m.file);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Every registered model + whether it's already downloaded. */
|
|
117
|
+
export function listWhisperModels(): WhisperModelStatus[] {
|
|
118
|
+
return WHISPER_MODELS.map((m) => {
|
|
119
|
+
const p = whisperModelPath(m);
|
|
120
|
+
let downloaded = false;
|
|
121
|
+
let bytes: number | undefined;
|
|
122
|
+
try {
|
|
123
|
+
const st = statSync(p);
|
|
124
|
+
if (st.isFile() && st.size > 0) {
|
|
125
|
+
downloaded = true;
|
|
126
|
+
bytes = st.size;
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
/* not downloaded */
|
|
130
|
+
}
|
|
131
|
+
return { ...m, downloaded, ...(downloaded ? { path: p, bytes } : {}) };
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve a downloaded model's path for `maude design transcribe` (no --model
|
|
137
|
+
* needed once one is downloaded). Preference order: the caller-preferred id
|
|
138
|
+
* (from `generation.transcription.whisperModel`) → any downloaded MULTILINGUAL
|
|
139
|
+
* model (safe for non-English) → any downloaded model → null.
|
|
140
|
+
*/
|
|
141
|
+
export function resolveWhisperModel(preferId?: string): string | null {
|
|
142
|
+
const dir = whisperModelsDir();
|
|
143
|
+
if (!existsSync(dir)) return null;
|
|
144
|
+
let present: Set<string>;
|
|
145
|
+
try {
|
|
146
|
+
present = new Set(readdirSync(dir));
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const has = (m: WhisperModelDescriptor) => present.has(m.file);
|
|
151
|
+
if (preferId) {
|
|
152
|
+
const pref = getWhisperModel(preferId);
|
|
153
|
+
if (pref && has(pref)) return join(dir, pref.file);
|
|
154
|
+
}
|
|
155
|
+
const multi = WHISPER_MODELS.find((m) => m.multilingual && has(m));
|
|
156
|
+
if (multi) return join(dir, multi.file);
|
|
157
|
+
const any = WHISPER_MODELS.find(has);
|
|
158
|
+
return any ? join(dir, any.file) : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Download one model into the managed cache, streamed with a progress callback
|
|
163
|
+
* (Task 2.7). Egress discipline: the URL is derived from the FROZEN registry
|
|
164
|
+
* (initial host is always huggingface.co, asserted https + fixed host); the
|
|
165
|
+
* redirect the HF blob store issues is followed but the FINAL hop is re-asserted
|
|
166
|
+
* https + a `*.huggingface.co` host (ethical-hacker Finding 2); the body is
|
|
167
|
+
* size-capped per-chunk to the model's expected size (+20% margin) before it can
|
|
168
|
+
* fill the disk; written to a `.part` temp and atomically renamed so a
|
|
169
|
+
* partial/aborted download never looks complete. Pass an AbortSignal (a timeout)
|
|
170
|
+
* so a stalled connection can't wedge the single download slot. Not a full
|
|
171
|
+
* `_fetch-asset.mjs` (no resolved-IP private-range guard) — proportionate because
|
|
172
|
+
* the host is pinned to HF and the route is loopback + same-origin only. Throws
|
|
173
|
+
* on any failure (the temp file is cleaned up); the caller owns progress state.
|
|
174
|
+
*/
|
|
175
|
+
export async function downloadWhisperModel(
|
|
176
|
+
id: string,
|
|
177
|
+
onProgress: (received: number, total: number) => void,
|
|
178
|
+
signal?: AbortSignal
|
|
179
|
+
): Promise<string> {
|
|
180
|
+
const m = getWhisperModel(id);
|
|
181
|
+
if (!m) throw new Error(`unknown whisper model: ${id}`);
|
|
182
|
+
const url = whisperModelUrl(m);
|
|
183
|
+
const u = new URL(url);
|
|
184
|
+
if (u.protocol !== 'https:') throw new Error('model URL must be https');
|
|
185
|
+
if (u.hostname !== 'huggingface.co') throw new Error('model host not allowlisted');
|
|
186
|
+
|
|
187
|
+
const dir = whisperModelsDir();
|
|
188
|
+
await mkdir(dir, { recursive: true });
|
|
189
|
+
const finalPath = join(dir, m.file);
|
|
190
|
+
const tmpPath = `${finalPath}.part`;
|
|
191
|
+
|
|
192
|
+
const cap = Math.ceil(m.sizeMB * 1.2) * 1024 * 1024; // expected size + 20% margin
|
|
193
|
+
// huggingface.co 302-redirects model blobs to its own LFS CDN, so we must
|
|
194
|
+
// follow — but re-assert the FINAL hop is still https + a huggingface.co host
|
|
195
|
+
// (ethical-hacker Finding 2): an open-redirect/on-path must not land the fetch
|
|
196
|
+
// on an arbitrary host. A wall-clock timeout is layered by the caller's signal.
|
|
197
|
+
const res = await fetch(url, { signal, redirect: 'follow' });
|
|
198
|
+
try {
|
|
199
|
+
const finalUrl = new URL(res.url || url);
|
|
200
|
+
if (finalUrl.protocol !== 'https:' || !/(^|\.)huggingface\.co$/.test(finalUrl.hostname))
|
|
201
|
+
throw new Error(`model download redirected off huggingface.co (${finalUrl.hostname})`);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (err instanceof Error && err.message.startsWith('model download redirected')) throw err;
|
|
204
|
+
throw new Error('model download resolved to an invalid URL');
|
|
205
|
+
}
|
|
206
|
+
if (!res.ok) throw new Error(`model download failed: HTTP ${res.status}`);
|
|
207
|
+
const declared = Number(res.headers.get('content-length'));
|
|
208
|
+
if (Number.isFinite(declared) && declared > cap)
|
|
209
|
+
throw new Error(`model larger than expected (${declared} > ${cap} bytes)`);
|
|
210
|
+
const total = Number.isFinite(declared) && declared > 0 ? declared : m.sizeMB * 1024 * 1024;
|
|
211
|
+
|
|
212
|
+
const body = res.body;
|
|
213
|
+
if (!body) throw new Error('empty model response');
|
|
214
|
+
const reader = body.getReader();
|
|
215
|
+
const writer = Bun.file(tmpPath).writer();
|
|
216
|
+
let received = 0;
|
|
217
|
+
try {
|
|
218
|
+
for (;;) {
|
|
219
|
+
const { done, value } = await reader.read();
|
|
220
|
+
if (done) break;
|
|
221
|
+
if (value) {
|
|
222
|
+
received += value.byteLength;
|
|
223
|
+
if (received > cap) throw new Error(`model exceeded ${cap} bytes`);
|
|
224
|
+
writer.write(value);
|
|
225
|
+
onProgress(received, total);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
await writer.end();
|
|
229
|
+
if (received === 0) throw new Error('model download was empty');
|
|
230
|
+
await rename(tmpPath, finalPath);
|
|
231
|
+
return finalPath;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
try {
|
|
234
|
+
await writer.end();
|
|
235
|
+
} catch {
|
|
236
|
+
/* ignore */
|
|
237
|
+
}
|
|
238
|
+
await rm(tmpPath, { force: true }).catch(() => {});
|
|
239
|
+
throw err;
|
|
240
|
+
} finally {
|
|
241
|
+
try {
|
|
242
|
+
reader.releaseLock();
|
|
243
|
+
} catch {
|
|
244
|
+
/* already released */
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Remove a downloaded model (reclaim disk). Returns true if a file was removed. */
|
|
250
|
+
export async function removeWhisperModel(id: string): Promise<boolean> {
|
|
251
|
+
const m = getWhisperModel(id);
|
|
252
|
+
if (!m) return false;
|
|
253
|
+
const p = whisperModelPath(m);
|
|
254
|
+
if (!existsSync(p)) return false;
|
|
255
|
+
await rm(p, { force: true });
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Task 2.7 feasibility spike — transformers.js whisper (word timestamps in the sidecar)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-11 · **Spike verdict: B is functionally feasible.** · **Build outcome: approach A shipped** (B blocked on a native-dep / DDR-070 conflict — see the resolution note at the bottom).
|
|
4
|
+
|
|
5
|
+
The plan gated the one-click local-subtitle build on a spike (DDR-164 Open-Question 7):
|
|
6
|
+
in-process transformers.js whisper must produce word-level timestamps, at tolerable
|
|
7
|
+
speed + memory, running MAIN-origin / in the sidecar (not the untrusted canvas realm).
|
|
8
|
+
|
|
9
|
+
## Result (Node sidecar, `@huggingface/transformers` v3, `onnx-community/whisper-base_timestamped`, q8)
|
|
10
|
+
|
|
11
|
+
| Criterion | Measured | Gate |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| Word-level timestamps | `hasWordTimestamps: true` — `"And" [0.22–0.52] "so" [0.52–0.84] "my" [0.84–1.2]…` | ✅ |
|
|
14
|
+
| Transcript accuracy | "And so my fellow Americans ask not what your country can do for you…" (JFK 11s clip, verbatim) | ✅ |
|
|
15
|
+
| Speed | 2.66× realtime (4.14 s for 11.0 s audio) | ✅ tolerable; show an ETA for long clips |
|
|
16
|
+
| Memory | ~676 MB peak RSS | ✅ fits |
|
|
17
|
+
| In-process, main-origin | Node/Bun via onnxruntime-node — no browser CSP surface | ✅ |
|
|
18
|
+
|
|
19
|
+
## Build notes (feed the DDR + the implementation)
|
|
20
|
+
|
|
21
|
+
1. **Must use a `_timestamped` model variant.** Plain `onnx-community/whisper-base`
|
|
22
|
+
throws `Model outputs must contain cross attentions to extract timestamps` —
|
|
23
|
+
word timestamps need the cross-attention export. Use `onnx-community/whisper-*_timestamped`.
|
|
24
|
+
2. **Model size default.** `base` (q8) is the sweet spot for the WASM/sidecar default
|
|
25
|
+
(accuracy vs download size vs the 2.66× speed). Offer `tiny`/`small` in the size picker.
|
|
26
|
+
3. **onnxruntime-node teardown quirk.** A cosmetic `mutex lock failed` SIGABRT fires on
|
|
27
|
+
process exit AFTER all output is produced — the shim must `process.exit(0)` cleanly
|
|
28
|
+
once the SRT is written (the inference itself is unaffected).
|
|
29
|
+
4. **Packaging fork (the genuinely consequential decision — DDR-worthy).** transformers.js
|
|
30
|
+
in Node defaults to **onnxruntime-node** (native `.node` binaries) — fast, but a heavy
|
|
31
|
+
per-platform native dep that fights the `bun --compile` standalone-binary distribution
|
|
32
|
+
(DDR-045) and bloats the npm tarball. Alternatives that avoid that: (a) transformers.js
|
|
33
|
+
with **onnxruntime-web (WASM)** in the browser proof-canvas harness — mirrors the shipped
|
|
34
|
+
`@imgly/background-removal` precedent (DDR-161) exactly, no native dep, but slower and
|
|
35
|
+
in a browser context; (b) native **whisper.cpp + a managed model-download button** (the
|
|
36
|
+
plan's fallback A) — fastest, but needs the binary. This fork is surfaced to the owner
|
|
37
|
+
before the heavy dep ships to every user.
|
|
38
|
+
|
|
39
|
+
## Repro
|
|
40
|
+
|
|
41
|
+
`scratchpad/whisper-spike/spike3.mjs` (stereo→mono + 44.1k→16k resample; the earlier
|
|
42
|
+
garbled "[BIRDS CHIRPING]" run was a WAV-parse bug in the harness, not the model).
|
|
43
|
+
|
|
44
|
+
## Resolution (2026-07-11) — shipped approach A, not B
|
|
45
|
+
|
|
46
|
+
Building B revealed the packaging fork wasn't a preference but a **blocker**:
|
|
47
|
+
`@huggingface/transformers@3.8.1` hard-depends on **`sharp` + `onnxruntime-node`**,
|
|
48
|
+
the exact native "bun-compile-hostile" class **DDR-070 excludes**. A clean `npm add`
|
|
49
|
+
is impossible; B would require vendoring the prebuilt browser bundle + allowlisting
|
|
50
|
+
`cdn.jsdelivr.net` (WASM) and `huggingface.co` (models) in the canvas CSP — a
|
|
51
|
+
DDR-worthy vendoring + CDN commitment. `device:'wasm'` is also unsupported in the
|
|
52
|
+
transformers.js **Node** build (only `cpu` = onnxruntime-node), so sidecar-WASM is
|
|
53
|
+
out — B would have to run in a browser proof-canvas harness (the @imgly pattern).
|
|
54
|
+
|
|
55
|
+
**Owner chose approach A:** keep the fast native whisper.cpp engine (already validated
|
|
56
|
+
on real footage) and remove the *friction* instead — a one-click managed model
|
|
57
|
+
download (`generation/whisper-models.ts` + `/_api/generate/whisper-model` + a Settings
|
|
58
|
+
card), `--provider whisper` model auto-resolution, and auto-ffmpeg container decode.
|
|
59
|
+
No new runtime dependency; native binary still required (brew / build). True
|
|
60
|
+
binary-free zero-install (B) stays a documented future option behind the vendoring
|
|
61
|
+
decision.
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// echoed, or persisted (it is stripped from the JSON we return on error).
|
|
15
15
|
|
|
16
16
|
import type { Context } from '../context.ts';
|
|
17
|
+
import { parseGitHubRemote } from '../github/endpoints.ts';
|
|
18
|
+
import { createPullRequest, GitHubApiError } from '../github/service.ts';
|
|
17
19
|
import { getGithubToken } from '../github/token.ts';
|
|
18
20
|
import {
|
|
19
21
|
type GitFileStatus,
|
|
@@ -284,19 +286,64 @@ export function createGitEndpoints(ctx: Context): GitEndpoints {
|
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
async function fold(body: unknown): Promise<GitEndpointResult> {
|
|
287
|
-
// Token resolution mirrors push
|
|
288
|
-
//
|
|
289
|
+
// Token resolution mirrors push: body token → keychain bridge → undefined. Needed
|
|
290
|
+
// for the GitHub PR API call; an ssh draft-push authenticates with the user's key.
|
|
289
291
|
const token = readToken(body) ?? (await getGithubToken()) ?? undefined;
|
|
290
292
|
const b = (body ?? {}) as { name?: unknown; remote?: unknown };
|
|
291
293
|
const name = safeGitArg(b.name);
|
|
292
294
|
if (name === undefined) return bad('Invalid draft name.');
|
|
293
295
|
if (b.remote != null && safeRemoteArg(b.remote) === undefined) return bad('Invalid remote.');
|
|
294
296
|
const res = await gitFoldDraft(dir, name, token, { remote: safeRemoteArg(b.remote) });
|
|
295
|
-
if (res.ok)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
297
|
+
if (!res.ok) {
|
|
298
|
+
if (res.authRequired)
|
|
299
|
+
return { status: 401, json: { ok: false, authRequired: true, error: res.error } };
|
|
300
|
+
if (res.conflict)
|
|
301
|
+
return { status: 409, json: { ok: false, conflict: true, error: res.error } };
|
|
302
|
+
return { status: 502, json: { ok: false, error: res.error ?? 'Could not add the draft.' } };
|
|
303
|
+
}
|
|
304
|
+
// Local-merge path (no GitHub remote): the draft is already in the Shared version.
|
|
305
|
+
if (!res.prReady) return { status: 200, json: { ok: true, shared: res.shared } };
|
|
306
|
+
// PR path: the draft branch is pushed — open a pull request draft→shared. The merge
|
|
307
|
+
// itself happens on GitHub after review (this is how a protected `main` is added to).
|
|
308
|
+
const gh = res.remoteUrl ? parseGitHubRemote(res.remoteUrl) : null;
|
|
309
|
+
if (!gh)
|
|
310
|
+
return draftPublishedNoPr(
|
|
311
|
+
res.shared,
|
|
312
|
+
res.head,
|
|
313
|
+
'Draft published. Open a pull request on your Git host to add it to the Shared version.'
|
|
314
|
+
);
|
|
315
|
+
if (!token)
|
|
316
|
+
return draftPublishedNoPr(
|
|
317
|
+
res.shared,
|
|
318
|
+
res.head,
|
|
319
|
+
'Draft published. Sign in with GitHub in Maude to open the pull request.'
|
|
320
|
+
);
|
|
321
|
+
try {
|
|
322
|
+
const pr = await createPullRequest(token, gh.owner, gh.repo, {
|
|
323
|
+
head: res.head as string,
|
|
324
|
+
base: res.base as string,
|
|
325
|
+
title: `Add draft “${res.head}” to ${res.base}`,
|
|
326
|
+
body: 'Opened from Maude — “Add to Shared version”.',
|
|
327
|
+
});
|
|
328
|
+
return {
|
|
329
|
+
status: 200,
|
|
330
|
+
// Surface the resolved destination repo (F2): the user should SEE which
|
|
331
|
+
// owner/repo their draft was pushed to / PR'd against, not just "the Shared
|
|
332
|
+
// version" — a poisoned `origin` would otherwise silently target another repo.
|
|
333
|
+
json: {
|
|
334
|
+
ok: true,
|
|
335
|
+
shared: res.shared,
|
|
336
|
+
prUrl: pr.html_url,
|
|
337
|
+
prNumber: pr.number,
|
|
338
|
+
repo: `${gh.owner}/${gh.repo}`,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
} catch (e) {
|
|
342
|
+
// The draft IS pushed — a PR-creation failure is a partial success, not a hard
|
|
343
|
+
// fail. Surface the reason and keep the draft so the user can retry / open it.
|
|
344
|
+
const msg = e instanceof GitHubApiError ? e.message : 'Could not open the pull request.';
|
|
345
|
+
return draftPublishedNoPr(res.shared, res.head, msg);
|
|
346
|
+
}
|
|
300
347
|
}
|
|
301
348
|
|
|
302
349
|
async function fetchRemote(body: unknown): Promise<GitEndpointResult> {
|
|
@@ -335,6 +382,20 @@ function readToken(body: unknown): string | null {
|
|
|
335
382
|
return typeof t === 'string' && t.length > 0 ? t : null;
|
|
336
383
|
}
|
|
337
384
|
|
|
385
|
+
/** Fold partial-success: the draft branch was pushed but no PR could be opened (no
|
|
386
|
+
* sign-in, a non-GitHub remote, or the PR call failed). `ok:true` because the work IS
|
|
387
|
+
* published; the UI shows `error` as a heads-up and keeps the draft for a retry. */
|
|
388
|
+
function draftPublishedNoPr(
|
|
389
|
+
shared: string | undefined,
|
|
390
|
+
head: string | undefined,
|
|
391
|
+
error: string
|
|
392
|
+
): GitEndpointResult {
|
|
393
|
+
return {
|
|
394
|
+
status: 200,
|
|
395
|
+
json: { ok: true, shared, published: head, prUnavailable: true, error },
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
338
399
|
/** DDR-112 — expand each selected path to include any DIRTY same-directory,
|
|
339
400
|
* same-stem sidecar (`<stem>.meta.json`, `<stem>.annotations.svg`, …). The `.`
|
|
340
401
|
* delimiter prevents `ui/Pricing` from grabbing `ui/Pricing v3.*`. */
|