mixdog 0.9.156 → 0.9.157
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/package.json +6 -8
- package/scripts/build-ffmpeg-runtime.sh +141 -0
- package/scripts/generate-voice-runtime-manifest.mjs +70 -0
- package/scripts/lib/embedding-model-bench-core.mjs +353 -0
- package/scripts/lib/stage-postgres-runtime-windows.ps1 +224 -0
- package/scripts/native-tool-download.mjs +6 -7
- package/scripts/prepare-native-assets.mjs +13 -33
- package/scripts/prune-desktop-runtime.mjs +304 -0
- package/scripts/prune-embedding-runtime.mjs +19 -0
- package/scripts/run-office-live-tests.mjs +17 -21
- package/scripts/sync-voice-runtime-manifest.mjs +1 -0
- package/scripts/verify-release-assets.mjs +0 -63
- package/scripts/verify-voice-runtime-release.mjs +26 -8
- package/scripts/voice-runtime-config.json +2 -1
- package/src/app.mjs +0 -1
- package/src/headless-command.mjs +1 -2
- package/src/headless-exec.mjs +2 -5
- package/src/help.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +40 -15
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-replay-blocks.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +14 -7
- package/src/runtime/agent/orchestrator/providers/model-catalog-projection.mjs +160 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +14 -8
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +1 -28
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +30 -2
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +19 -277
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +21 -1
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +19 -5
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +5 -7
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +48 -15
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +203 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +29 -7
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +38 -1
- package/src/runtime/agent/orchestrator/session/thinking-replay-recovery.mjs +113 -0
- package/src/runtime/agent/orchestrator/session/token-estimate.mjs +130 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +17 -22
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +29 -5
- package/src/runtime/agent/orchestrator/tools/builtin/search-admission.mjs +33 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +57 -7
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/browser-bridge/action-schema.mjs +227 -30
- package/src/runtime/browser-bridge/browser-action-contract.d.mts +4 -0
- package/src/runtime/browser-bridge/browser-action-contract.mjs +25 -0
- package/src/runtime/browser-bridge/client.mjs +119 -35
- package/src/runtime/browser-bridge/tool-defs.mjs +40 -21
- package/src/runtime/channels/data/voice-runtime-manifest.json +35 -35
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +13 -1
- package/src/runtime/computer-bridge/action-schema.mjs +108 -21
- package/src/runtime/computer-bridge/client.mjs +55 -9
- package/src/runtime/computer-bridge/tool-defs.mjs +5 -1
- package/src/runtime/memory/data/runtime-manifest.json +3 -3
- package/src/runtime/memory/index.mjs +43 -11
- package/src/runtime/memory/lib/embedding-cache-retention.mjs +55 -0
- package/src/runtime/memory/lib/embedding-model-cache-compression.mjs +50 -0
- package/src/runtime/memory/lib/embedding-model-config.mjs +16 -12
- package/src/runtime/memory/lib/embedding-provider.mjs +9 -1
- package/src/runtime/memory/lib/embedding-reindex.mjs +54 -0
- package/src/runtime/memory/lib/embedding-worker.mjs +18 -5
- package/src/runtime/memory/lib/http-router.mjs +10 -14
- package/src/runtime/memory/lib/light-ko-morph.mjs +88 -0
- package/src/runtime/memory/lib/memory-embed.mjs +12 -30
- package/src/runtime/memory/lib/memory-recall-store.mjs +22 -167
- package/src/runtime/memory/lib/memory-text-utils.mjs +14 -56
- package/src/runtime/memory/lib/query-handlers.mjs +32 -39
- package/src/runtime/memory/lib/recall-embedding-readiness.mjs +78 -0
- package/src/runtime/memory/lib/recall-format.mjs +6 -2
- package/src/runtime/memory/lib/recall-fusion.mjs +53 -0
- package/src/runtime/memory/lib/recall-scoring.mjs +2 -140
- package/src/runtime/memory/lib/trace-mode.mjs +58 -0
- package/src/runtime/memory/lib/trace-store.mjs +75 -43
- package/src/runtime/office/assurance-benchmark.mjs +12 -0
- package/src/runtime/office/assurance.mjs +45 -5
- package/src/runtime/office/benchmark.mjs +6 -1
- package/src/runtime/office/capabilities.mjs +16 -12
- package/src/runtime/office/composition-system.mjs +33 -0
- package/src/runtime/office/content-model.mjs +0 -1
- package/src/runtime/office/design-docx-components.mjs +264 -0
- package/src/runtime/office/design-docx.mjs +203 -0
- package/src/runtime/office/design-library-core.mjs +252 -0
- package/src/runtime/office/design-library-pack.mjs +374 -0
- package/src/runtime/office/design-library.mjs +26 -1268
- package/src/runtime/office/design-pptx-executive.mjs +335 -0
- package/src/runtime/office/design-pptx-plan.mjs +891 -0
- package/src/runtime/office/design-pptx-primitives.mjs +87 -0
- package/src/runtime/office/design-pptx.mjs +776 -0
- package/src/runtime/office/design-review.mjs +373 -0
- package/src/runtime/office/design-system.mjs +43 -1988
- package/src/runtime/office/design-template-index.mjs +266 -0
- package/src/runtime/office/design-template-inspect.mjs +434 -0
- package/src/runtime/office/design-tokens.mjs +431 -0
- package/src/runtime/office/design-xlsx-components.mjs +171 -0
- package/src/runtime/office/design-xlsx.mjs +364 -0
- package/src/runtime/office/document-preview.mjs +139 -0
- package/src/runtime/office/index.mjs +14 -2191
- package/src/runtime/office/office-actions.mjs +940 -0
- package/src/runtime/office/office-com-host.ps1 +439 -73
- package/src/runtime/office/office-com-session-host.ps1 +57 -8
- package/src/runtime/office/office-core.mjs +270 -0
- package/src/runtime/office/office-documents.mjs +103 -0
- package/src/runtime/office/office-sessions.mjs +465 -0
- package/src/runtime/office/office-transactions.mjs +432 -0
- package/src/runtime/office/pdf-analysis.mjs +57 -0
- package/src/runtime/office/pdf-render.mjs +4 -1
- package/src/runtime/office/polish-benchmark.mjs +63 -15
- package/src/runtime/office/portable-cells.mjs +271 -0
- package/src/runtime/office/portable-docx-parts.mjs +326 -0
- package/src/runtime/office/portable-docx-xml.mjs +258 -0
- package/src/runtime/office/portable-docx.mjs +755 -0
- package/src/runtime/office/portable-ooxml.mjs +21 -6057
- package/src/runtime/office/portable-opc.mjs +350 -0
- package/src/runtime/office/portable-pivot.mjs +229 -0
- package/src/runtime/office/portable-pptx-chart.mjs +101 -0
- package/src/runtime/office/portable-pptx-charts.mjs +292 -0
- package/src/runtime/office/portable-pptx-core.mjs +316 -0
- package/src/runtime/office/portable-pptx-deck.mjs +314 -0
- package/src/runtime/office/portable-pptx-package.mjs +439 -0
- package/src/runtime/office/portable-pptx-shapes.mjs +607 -0
- package/src/runtime/office/portable-pptx.mjs +66 -0
- package/src/runtime/office/portable-sheet-parts.mjs +152 -0
- package/src/runtime/office/portable-sheet-xml.mjs +320 -0
- package/src/runtime/office/portable-slide-shapes.mjs +0 -4
- package/src/runtime/office/portable-snapshot.mjs +528 -0
- package/src/runtime/office/portable-soffice.mjs +293 -0
- package/src/runtime/office/portable-validation.mjs +734 -0
- package/src/runtime/office/portable-xlsx.mjs +848 -0
- package/src/runtime/office/portable-xml.mjs +234 -0
- package/src/runtime/office/quality-live-benchmark.mjs +119 -16
- package/src/runtime/office/quality-pipeline.mjs +0 -5
- package/src/runtime/office/snapshot-contract.mjs +190 -0
- package/src/runtime/office/text-metrics.mjs +5 -0
- package/src/runtime/office/tool-defs.mjs +2 -2
- package/src/runtime/shared/bridge-tool-args.mjs +42 -0
- package/src/runtime/shared/heap-cap.mjs +56 -0
- package/src/runtime/shared/idle-gc.mjs +150 -0
- package/src/runtime/shared/native-tool-paths.mjs +0 -1
- package/src/runtime/shared/tool-workload-gates.mjs +12 -0
- package/src/session-runtime/context-status.mjs +10 -1
- package/src/session-runtime/goal-runtime.mjs +38 -13
- package/src/session-runtime/prewarm.mjs +0 -6
- package/src/session-runtime/runtime-core.mjs +8 -0
- package/src/session-runtime/session-lifecycle.mjs +2 -0
- package/src/session-runtime/tool-profile.mjs +57 -0
- package/src/session-runtime/tool-surface.mjs +19 -3
- package/src/standalone/channel-worker.mjs +3 -1
- package/src/standalone/daemon.mjs +28 -4
- package/src/standalone/memory-runtime-proxy.mjs +2 -1
- package/src/standalone/session-client.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +2 -28
- package/src/standalone/session-runtime-worker.mjs +1 -2
- package/scripts/build-token-addon.mjs +0 -32
- package/src/runtime/agent/orchestrator/session/token-bpe.mjs +0 -42
- package/src/runtime/agent/orchestrator/session/token-native-worker.mjs +0 -35
- package/src/runtime/agent/orchestrator/session/token-native.mjs +0 -288
- package/src/runtime/agent/orchestrator/tools/token-addon-fetcher.mjs +0 -195
- package/src/runtime/agent/orchestrator/tools/token-manifest.json +0 -26
- package/src/runtime/memory/lib/ko-morph.mjs +0 -313
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.157",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"smoke:all": "npm run smoke && npm run smoke:boot && npm run smoke:patch && npm run smoke:compact",
|
|
55
55
|
"smoke:boot": "node scripts/boot-smoke.mjs",
|
|
56
56
|
"smoke:compact": "node scripts/compact-smoke.mjs",
|
|
57
|
-
"test:compact": "node --test scripts/suite-compact-test.mjs",
|
|
57
|
+
"test:compact": "node --test scripts/suite-compact-test.mjs src/runtime/agent/orchestrator/session/token-estimate.test.mjs",
|
|
58
58
|
"test:markdown": "node --test scripts/markdown-surface-test.mjs",
|
|
59
59
|
"smoke:loop": "node scripts/smoke-loop.mjs",
|
|
60
60
|
"smoke:loop:final": "node scripts/smoke-loop-report.mjs --require-complete --min-elapsed 5h --min-iterations 400 --max-gap 60s --max-smoke-ms 10000 --max-avg-smoke-ms 8500 --max-step-ms smoke.mjs=4000 --max-step-ms boot-smoke.mjs=8000 --max-rss-mb 140 --max-rss-growth-mb 50",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"test:session-transport": "node --test scripts/session-transport-test.mjs scripts/daemon-bootstrap-test.mjs src/runtime/shared/child-spawn-remote.test.mjs src/standalone/session-runtime-agent-control-client.test.mjs src/standalone/session-runtime-dispatch-cancel.test.mjs src/standalone/session-runtime-host-factory.test.mjs src/standalone/session-runtime-inline-host.test.mjs src/standalone/session-runtime-provider-cooldown.test.mjs src/standalone/session-runtime-shard-host.test.mjs src/standalone/session-runtime-shard-router.test.mjs",
|
|
73
73
|
"test:session": "node --test scripts/runtime-turn-contract-test.mjs scripts/session-save-fault-store-test.mjs scripts/turn-review-revert-test.mjs src/runtime/shared/tool-surface.test.mjs src/runtime/bridge-clients.test.mjs",
|
|
74
74
|
"test:quick": "npm run test:session && npm run test:session-transport && npm run test:desktop-main --prefix apps/desktop",
|
|
75
|
-
"test:office": "node --test src/runtime/office/office-runtime.test.mjs src/runtime/office/office-assurance.test.mjs src/runtime/office/portable-authoring.test.mjs && npm run test:office-approval --prefix apps/desktop",
|
|
75
|
+
"test:office": "node --test scripts/run-office-live-tests.test.mjs src/runtime/office/office-runtime.test.mjs src/runtime/office/office-assurance.test.mjs src/runtime/office/portable-authoring.test.mjs && npm run test:office-approval --prefix apps/desktop",
|
|
76
76
|
"test:office:live": "node scripts/run-office-live-tests.mjs",
|
|
77
77
|
"test:media": "node --test src/runtime/media/store.test.mjs src/runtime/media/renditions.test.mjs src/runtime/media/adapters/codex-image.test.mjs",
|
|
78
78
|
"failures": "node scripts/tool-failures.mjs",
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"dependencies": {
|
|
126
126
|
"@anthropic-ai/sdk": "^0.105.0",
|
|
127
127
|
"@google/generative-ai": "^0.24.1",
|
|
128
|
-
"@huggingface/transformers": "^
|
|
128
|
+
"@huggingface/transformers": "^4.2.0",
|
|
129
129
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
130
130
|
"@mozilla/readability": "^0.6.0",
|
|
131
131
|
"@napi-rs/canvas": "^1.0.8",
|
|
@@ -136,10 +136,9 @@
|
|
|
136
136
|
"ink": "^7.1.0",
|
|
137
137
|
"jsdom": "^27.0.0",
|
|
138
138
|
"jszip": "^3.10.1",
|
|
139
|
-
"kiwi-nlp": "^0.21.0",
|
|
140
139
|
"marked": "^14.1.4",
|
|
141
140
|
"node-cron": "^4.5.0",
|
|
142
|
-
"onnxruntime-node": "1.
|
|
141
|
+
"onnxruntime-node": "1.29.0",
|
|
143
142
|
"openai": "^6.44.0",
|
|
144
143
|
"pdf-lib": "^1.17.1",
|
|
145
144
|
"pdfjs-dist": "^6.2.108",
|
|
@@ -150,7 +149,6 @@
|
|
|
150
149
|
"string-width": "^8.2.1",
|
|
151
150
|
"strip-ansi": "^7.2.0",
|
|
152
151
|
"tesseract.js": "^7.0.0",
|
|
153
|
-
"tiktoken": "^1.0.22",
|
|
154
152
|
"undici": "^8.5.0",
|
|
155
153
|
"unpdf": "^1.8.0",
|
|
156
154
|
"wrap-ansi": "^10.0.0",
|
|
@@ -160,7 +158,7 @@
|
|
|
160
158
|
},
|
|
161
159
|
"overrides": {
|
|
162
160
|
"onnxruntime-node": {
|
|
163
|
-
".": "1.
|
|
161
|
+
".": "1.29.0",
|
|
164
162
|
"global-agent": "^4.1.3"
|
|
165
163
|
},
|
|
166
164
|
"sharp": "^0.35.3"
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
FFMPEG_VERSION="${FFMPEG_VERSION:-6.1.1}"
|
|
5
|
+
TARGET_OS="${TARGET_OS:?TARGET_OS is required}"
|
|
6
|
+
TARGET_ARCH="${TARGET_ARCH:?TARGET_ARCH is required}"
|
|
7
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
8
|
+
BUILD_DIR="$ROOT_DIR/build/ffmpeg-runtime-${TARGET_OS}-${TARGET_ARCH}"
|
|
9
|
+
SOURCE_DIR="$BUILD_DIR/ffmpeg-${FFMPEG_VERSION}"
|
|
10
|
+
DIST_DIR="$ROOT_DIR/dist"
|
|
11
|
+
|
|
12
|
+
rm -rf "$BUILD_DIR"
|
|
13
|
+
mkdir -p "$BUILD_DIR" "$DIST_DIR"
|
|
14
|
+
|
|
15
|
+
echo "==> Downloading FFmpeg n${FFMPEG_VERSION}"
|
|
16
|
+
curl -fsSL --retry 3 \
|
|
17
|
+
"https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz" \
|
|
18
|
+
-o "$BUILD_DIR/ffmpeg.tar.xz"
|
|
19
|
+
tar -xJf "$BUILD_DIR/ffmpeg.tar.xz" -C "$BUILD_DIR"
|
|
20
|
+
|
|
21
|
+
CONFIGURE_FLAGS=(
|
|
22
|
+
--disable-everything
|
|
23
|
+
--disable-autodetect
|
|
24
|
+
--disable-debug
|
|
25
|
+
--disable-doc
|
|
26
|
+
--disable-network
|
|
27
|
+
--disable-shared
|
|
28
|
+
--enable-static
|
|
29
|
+
--enable-small
|
|
30
|
+
--enable-ffmpeg
|
|
31
|
+
--enable-avcodec
|
|
32
|
+
--enable-avfilter
|
|
33
|
+
--enable-avformat
|
|
34
|
+
--enable-swresample
|
|
35
|
+
--enable-protocol=file,pipe
|
|
36
|
+
--enable-demuxer=aac,ac3,aiff,amr,ape,asf,au,avi,caf,flac,flv,matroska,mov,mp3,mpegts,ogg,wav,wv
|
|
37
|
+
--enable-decoder=aac,aac_fixed,ac3,alac,amrnb,amrwb,ape,eac3,flac,mp1,mp1float,mp2,mp2float,mp3,mp3float,opus,pcm_alaw,pcm_f32be,pcm_f32le,pcm_f64be,pcm_f64le,pcm_mulaw,pcm_s16be,pcm_s16le,pcm_s24be,pcm_s24le,pcm_s32be,pcm_s32le,pcm_s8,pcm_u16be,pcm_u16le,pcm_u24be,pcm_u24le,pcm_u32be,pcm_u32le,pcm_u8,vorbis,wavpack,wmav1,wmav2,wmapro
|
|
38
|
+
--enable-encoder=pcm_s16le
|
|
39
|
+
--enable-parser=aac,aac_latm,ac3,flac,mpegaudio,opus,vorbis
|
|
40
|
+
--enable-filter=aformat,anull,aresample
|
|
41
|
+
--enable-muxer=wav
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
EXE_NAME=ffmpeg
|
|
45
|
+
STRIP_COMMAND=strip
|
|
46
|
+
if [[ "$TARGET_OS" == "win32" ]]; then
|
|
47
|
+
CONFIGURE_FLAGS+=(
|
|
48
|
+
--target-os=mingw32
|
|
49
|
+
--arch=x86_64
|
|
50
|
+
--cc=gcc
|
|
51
|
+
--ar=ar
|
|
52
|
+
--ranlib=ranlib
|
|
53
|
+
--nm=nm
|
|
54
|
+
--strip=strip
|
|
55
|
+
--extra-ldflags=-static
|
|
56
|
+
)
|
|
57
|
+
EXE_NAME=ffmpeg.exe
|
|
58
|
+
elif [[ "$TARGET_OS" == "darwin" ]]; then
|
|
59
|
+
STRIP_COMMAND="strip -x"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
echo "==> Configuring minimal audio-transcode runtime"
|
|
63
|
+
pushd "$SOURCE_DIR" >/dev/null
|
|
64
|
+
./configure "${CONFIGURE_FLAGS[@]}"
|
|
65
|
+
make -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || sysctl -n hw.logicalcpu)"
|
|
66
|
+
popd >/dev/null
|
|
67
|
+
|
|
68
|
+
RUNTIME_EXE="$SOURCE_DIR/ffmpeg"
|
|
69
|
+
if [[ "$TARGET_OS" == "win32" ]]; then
|
|
70
|
+
RUNTIME_EXE="$SOURCE_DIR/ffmpeg.exe"
|
|
71
|
+
fi
|
|
72
|
+
if [[ ! -f "$RUNTIME_EXE" ]]; then
|
|
73
|
+
echo "FFmpeg build did not produce $RUNTIME_EXE" >&2
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
read -r -a STRIP_PARTS <<< "$STRIP_COMMAND"
|
|
78
|
+
"${STRIP_PARTS[@]}" "$RUNTIME_EXE"
|
|
79
|
+
|
|
80
|
+
echo "==> Smoke testing WAV normalization"
|
|
81
|
+
PYTHON_COMMAND=
|
|
82
|
+
for candidate in python3 python; do
|
|
83
|
+
if command -v "$candidate" >/dev/null 2>&1; then
|
|
84
|
+
PYTHON_COMMAND="$candidate"
|
|
85
|
+
break
|
|
86
|
+
fi
|
|
87
|
+
done
|
|
88
|
+
if [[ -z "$PYTHON_COMMAND" ]]; then
|
|
89
|
+
echo "Python 3 is required for the FFmpeg runtime smoke test" >&2
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
"$PYTHON_COMMAND" - "$BUILD_DIR/input.wav" <<'PY'
|
|
94
|
+
import math
|
|
95
|
+
import struct
|
|
96
|
+
import sys
|
|
97
|
+
import wave
|
|
98
|
+
|
|
99
|
+
with wave.open(sys.argv[1], "wb") as wav:
|
|
100
|
+
wav.setnchannels(2)
|
|
101
|
+
wav.setsampwidth(2)
|
|
102
|
+
wav.setframerate(48000)
|
|
103
|
+
frames = bytearray()
|
|
104
|
+
for index in range(4800):
|
|
105
|
+
sample = int(math.sin(index * 2 * math.pi * 440 / 48000) * 12000)
|
|
106
|
+
frames.extend(struct.pack("<hh", sample, sample))
|
|
107
|
+
wav.writeframes(frames)
|
|
108
|
+
PY
|
|
109
|
+
"$RUNTIME_EXE" -hide_banner -loglevel error \
|
|
110
|
+
-i "$BUILD_DIR/input.wav" -ar 16000 -ac 1 -threads 1 -y "$BUILD_DIR/output.wav"
|
|
111
|
+
"$PYTHON_COMMAND" - "$BUILD_DIR/output.wav" <<'PY'
|
|
112
|
+
import sys
|
|
113
|
+
import wave
|
|
114
|
+
|
|
115
|
+
with wave.open(sys.argv[1], "rb") as wav:
|
|
116
|
+
assert wav.getframerate() == 16000
|
|
117
|
+
assert wav.getnchannels() == 1
|
|
118
|
+
assert wav.getnframes() > 0
|
|
119
|
+
PY
|
|
120
|
+
|
|
121
|
+
RUNTIME_BYTES="$(wc -c < "$RUNTIME_EXE" | tr -d ' ')"
|
|
122
|
+
if (( RUNTIME_BYTES > 26214400 )); then
|
|
123
|
+
echo "Minimal FFmpeg exceeds 25 MiB: $RUNTIME_BYTES bytes" >&2
|
|
124
|
+
exit 1
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
ASSET_NAME="ffmpeg-${TARGET_OS}-${TARGET_ARCH}.gz"
|
|
128
|
+
gzip -9 -n -c "$RUNTIME_EXE" > "$DIST_DIR/$ASSET_NAME"
|
|
129
|
+
ASSET_BYTES="$(wc -c < "$DIST_DIR/$ASSET_NAME" | tr -d ' ')"
|
|
130
|
+
if (( ASSET_BYTES > 15728640 )); then
|
|
131
|
+
echo "Compressed FFmpeg exceeds 15 MiB: $ASSET_BYTES bytes" >&2
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
136
|
+
(cd "$DIST_DIR" && sha256sum "$ASSET_NAME" > "$ASSET_NAME.sha256")
|
|
137
|
+
else
|
|
138
|
+
(cd "$DIST_DIR" && shasum -a 256 "$ASSET_NAME" > "$ASSET_NAME.sha256")
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
echo "Built $DIST_DIR/$ASSET_NAME ($RUNTIME_BYTES bytes raw, $ASSET_BYTES bytes compressed)"
|
|
@@ -10,6 +10,7 @@ const RELEASE_HASH_INPUTS = [
|
|
|
10
10
|
new URL('../.github/workflows/build-voice-runtime.yml', import.meta.url),
|
|
11
11
|
new URL('./build-voice-runtime-unix.sh', import.meta.url),
|
|
12
12
|
new URL('./build-voice-runtime-windows.ps1', import.meta.url),
|
|
13
|
+
new URL('./build-ffmpeg-runtime.sh', import.meta.url),
|
|
13
14
|
new URL('./generate-voice-runtime-manifest.mjs', import.meta.url),
|
|
14
15
|
]
|
|
15
16
|
|
|
@@ -21,6 +22,9 @@ function loadVoiceRuntimeConfig() {
|
|
|
21
22
|
if (!/^[a-f0-9]{40}$/.test(config.whisperCommit || '')) {
|
|
22
23
|
throw new Error('voice runtime config has an invalid whisperCommit')
|
|
23
24
|
}
|
|
25
|
+
if (!/^\d+\.\d+\.\d+-mixdog\.\d+$/.test(config.ffmpegVersion || '')) {
|
|
26
|
+
throw new Error('voice runtime config has an invalid ffmpegVersion')
|
|
27
|
+
}
|
|
24
28
|
if (!Array.isArray(config.platforms) || config.platforms.length === 0) {
|
|
25
29
|
throw new Error('voice runtime config has no platforms')
|
|
26
30
|
}
|
|
@@ -133,6 +137,38 @@ export function buildVoiceRuntimePlatforms(
|
|
|
133
137
|
return platforms
|
|
134
138
|
}
|
|
135
139
|
|
|
140
|
+
function ffmpegAssetName(spec) {
|
|
141
|
+
return `ffmpeg-${spec.os}-${spec.arch}.gz`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function buildVoiceFfmpegPlatforms(
|
|
145
|
+
releaseAssets,
|
|
146
|
+
{ repository = 'tribgames/mixdog', tag = VOICE_RUNTIME_TAG } = {},
|
|
147
|
+
) {
|
|
148
|
+
const byName = new Map((releaseAssets || []).map((asset) => [asset.name, asset]))
|
|
149
|
+
const platforms = {}
|
|
150
|
+
for (const spec of VOICE_RUNTIME_CONFIG.platforms) {
|
|
151
|
+
const assetName = ffmpegAssetName(spec)
|
|
152
|
+
const asset = byName.get(assetName)
|
|
153
|
+
if (!asset) throw new Error(`release is missing required asset ${assetName}`)
|
|
154
|
+
if (!Number.isSafeInteger(asset.size) || asset.size <= 0) {
|
|
155
|
+
throw new Error(`release asset ${assetName} has invalid size`)
|
|
156
|
+
}
|
|
157
|
+
platforms[spec.key] = {
|
|
158
|
+
url: `https://github.com/${repository}/releases/download/${tag}/${assetName}`,
|
|
159
|
+
sha256: releaseAssetSha256(asset),
|
|
160
|
+
size: asset.size,
|
|
161
|
+
format: 'gz',
|
|
162
|
+
executable: spec.os === 'win32' ? 'ffmpeg.exe' : 'ffmpeg',
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
version: VOICE_RUNTIME_CONFIG.ffmpegVersion,
|
|
167
|
+
source: 'FFmpeg n6.1.1 minimal audio-transcode build',
|
|
168
|
+
platforms,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
136
172
|
function githubHeaders(token, accept = 'application/vnd.github+json') {
|
|
137
173
|
return {
|
|
138
174
|
Accept: accept,
|
|
@@ -207,6 +243,7 @@ export function voiceRuntimeIdentity(manifest) {
|
|
|
207
243
|
source: manifest?.source,
|
|
208
244
|
release_tag: manifest?.release_tag,
|
|
209
245
|
build_hash: manifest?.build_hash,
|
|
246
|
+
ffmpeg: manifest?.ffmpeg,
|
|
210
247
|
platforms: manifest?.platforms,
|
|
211
248
|
}
|
|
212
249
|
}
|
|
@@ -258,6 +295,36 @@ export async function assertVoiceRuntimeManifestIdentity(
|
|
|
258
295
|
throw new Error(`voice runtime manifest ${spec.key} does not match the release config`)
|
|
259
296
|
}
|
|
260
297
|
}
|
|
298
|
+
const expectedFfmpeg = buildVoiceFfmpegPlatforms(
|
|
299
|
+
VOICE_RUNTIME_CONFIG.platforms.map((spec, index) => ({
|
|
300
|
+
name: ffmpegAssetName(spec),
|
|
301
|
+
size: index + 1,
|
|
302
|
+
digest: `sha256:${String(index + 1).padStart(64, '0')}`,
|
|
303
|
+
})),
|
|
304
|
+
{ repository, tag: VOICE_RUNTIME_TAG },
|
|
305
|
+
)
|
|
306
|
+
if (
|
|
307
|
+
manifest?.ffmpeg?.version !== expectedFfmpeg.version
|
|
308
|
+
|| manifest?.ffmpeg?.source !== expectedFfmpeg.source
|
|
309
|
+
|| JSON.stringify(Object.keys(manifest?.ffmpeg?.platforms || {}))
|
|
310
|
+
!== JSON.stringify(Object.keys(expectedFfmpeg.platforms))
|
|
311
|
+
) {
|
|
312
|
+
throw new Error('voice runtime FFmpeg manifest does not match the release config')
|
|
313
|
+
}
|
|
314
|
+
for (const spec of VOICE_RUNTIME_CONFIG.platforms) {
|
|
315
|
+
const actual = manifest.ffmpeg.platforms[spec.key]
|
|
316
|
+
const expected = expectedFfmpeg.platforms[spec.key]
|
|
317
|
+
if (
|
|
318
|
+
actual?.url !== expected.url
|
|
319
|
+
|| actual?.format !== expected.format
|
|
320
|
+
|| actual?.executable !== expected.executable
|
|
321
|
+
|| !/^[a-f0-9]{64}$/.test(actual?.sha256 || '')
|
|
322
|
+
|| !Number.isSafeInteger(actual?.size)
|
|
323
|
+
|| actual.size <= 0
|
|
324
|
+
) {
|
|
325
|
+
throw new Error(`voice runtime FFmpeg ${spec.key} does not match the release config`)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
261
328
|
}
|
|
262
329
|
|
|
263
330
|
async function main() {
|
|
@@ -269,11 +336,13 @@ async function main() {
|
|
|
269
336
|
const manifestPath = resolve(process.argv[2] || 'src/runtime/channels/data/voice-runtime-manifest.json')
|
|
270
337
|
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
271
338
|
let platforms
|
|
339
|
+
let ffmpeg
|
|
272
340
|
let lastError
|
|
273
341
|
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
274
342
|
try {
|
|
275
343
|
const release = await fetchGithubRelease(repository, tag, process.env.GITHUB_TOKEN)
|
|
276
344
|
platforms = buildVoiceRuntimePlatforms(release.assets, { repository, tag })
|
|
345
|
+
ffmpeg = buildVoiceFfmpegPlatforms(release.assets, { repository, tag })
|
|
277
346
|
lastError = null
|
|
278
347
|
break
|
|
279
348
|
} catch (error) {
|
|
@@ -286,6 +355,7 @@ async function main() {
|
|
|
286
355
|
manifest.source = `ggml-org/whisper.cpp@${VOICE_RUNTIME_COMMIT}`
|
|
287
356
|
manifest.release_tag = tag
|
|
288
357
|
manifest.build_hash = await voiceRuntimeBuildHash()
|
|
358
|
+
manifest.ffmpeg = ffmpeg
|
|
289
359
|
manifest.platforms = platforms
|
|
290
360
|
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
291
361
|
console.log(`updated ${manifestPath} from ${tag}`)
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { parsePeriod } from '../../src/runtime/memory/lib/recall-format.mjs'
|
|
3
|
+
|
|
4
|
+
export const CASE_FILES = Object.freeze([
|
|
5
|
+
'recall-bench-cases.json',
|
|
6
|
+
'recall-quality-cases.json',
|
|
7
|
+
'recall-repo-cases.json',
|
|
8
|
+
'recall-usecase-cases.json',
|
|
9
|
+
'recall-event-cases.json',
|
|
10
|
+
'recall-history-cases.json',
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
const RETRIEVAL_INSTRUCTION = 'Given a web search query, retrieve relevant passages that answer the query'
|
|
14
|
+
|
|
15
|
+
export const MODEL_SPECS = Object.freeze({
|
|
16
|
+
harrier: Object.freeze({
|
|
17
|
+
label: 'Harrier 270M q4',
|
|
18
|
+
modelId: 'onnx-community/harrier-oss-v1-270m-ONNX',
|
|
19
|
+
dtype: 'q4',
|
|
20
|
+
pooling: 'last_token',
|
|
21
|
+
outputName: 'sentence_embedding',
|
|
22
|
+
queryPrefix: `Instruct: ${RETRIEVAL_INSTRUCTION}\nQuery: `,
|
|
23
|
+
documentPrefix: '',
|
|
24
|
+
batchSize: 16,
|
|
25
|
+
}),
|
|
26
|
+
granite97: Object.freeze({
|
|
27
|
+
label: 'Granite 97M quint8',
|
|
28
|
+
modelId: 'ibm-granite/granite-embedding-97m-multilingual-r2',
|
|
29
|
+
dtype: 'fp32',
|
|
30
|
+
modelFileName: 'model_quint8_avx2',
|
|
31
|
+
pooling: 'cls',
|
|
32
|
+
outputName: '',
|
|
33
|
+
queryPrefix: '',
|
|
34
|
+
documentPrefix: '',
|
|
35
|
+
batchSize: 16,
|
|
36
|
+
}),
|
|
37
|
+
e5small: Object.freeze({
|
|
38
|
+
label: 'multilingual-e5-small q8',
|
|
39
|
+
modelId: 'Xenova/multilingual-e5-small',
|
|
40
|
+
dtype: 'q8',
|
|
41
|
+
pooling: 'mean',
|
|
42
|
+
outputName: '',
|
|
43
|
+
queryPrefix: 'query: ',
|
|
44
|
+
documentPrefix: 'passage: ',
|
|
45
|
+
batchSize: 16,
|
|
46
|
+
}),
|
|
47
|
+
embeddinggemma: Object.freeze({
|
|
48
|
+
label: 'EmbeddingGemma 300M q4',
|
|
49
|
+
modelId: 'onnx-community/embeddinggemma-300m-ONNX',
|
|
50
|
+
dtype: 'q4',
|
|
51
|
+
pooling: 'mean',
|
|
52
|
+
outputName: 'sentence_embedding',
|
|
53
|
+
queryPrefix: 'task: search result | query: ',
|
|
54
|
+
documentPrefix: 'title: none | text: ',
|
|
55
|
+
batchSize: 16,
|
|
56
|
+
}),
|
|
57
|
+
bgem3: Object.freeze({
|
|
58
|
+
label: 'BGE-M3 q4',
|
|
59
|
+
modelId: 'Xenova/bge-m3',
|
|
60
|
+
dtype: 'q4',
|
|
61
|
+
pooling: 'cls',
|
|
62
|
+
outputName: '',
|
|
63
|
+
queryPrefix: '',
|
|
64
|
+
documentPrefix: '',
|
|
65
|
+
batchSize: 16,
|
|
66
|
+
}),
|
|
67
|
+
qwen3: Object.freeze({
|
|
68
|
+
label: 'Qwen3-Embedding 0.6B q8',
|
|
69
|
+
modelId: 'onnx-community/Qwen3-Embedding-0.6B-ONNX',
|
|
70
|
+
dtype: 'q8',
|
|
71
|
+
pooling: 'last_token',
|
|
72
|
+
outputName: '',
|
|
73
|
+
queryPrefix: `Instruct: ${RETRIEVAL_INSTRUCTION}\nQuery:`,
|
|
74
|
+
documentPrefix: '',
|
|
75
|
+
batchSize: 16,
|
|
76
|
+
}),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
export function documentText(row) {
|
|
80
|
+
return [row?.element, row?.summary].filter(Boolean).join(' — ').trim()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function classifyQueryLanguage(query) {
|
|
84
|
+
const text = String(query || '')
|
|
85
|
+
const hasHangul = /[\uac00-\ud7a3]/u.test(text)
|
|
86
|
+
const hasLatin = /[A-Za-z]/u.test(text)
|
|
87
|
+
if (hasHangul && hasLatin) return 'mixed'
|
|
88
|
+
if (hasHangul) return 'ko'
|
|
89
|
+
if (hasLatin) return 'en'
|
|
90
|
+
return 'other'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildEvaluation(caseFile, kase) {
|
|
94
|
+
const query = kase?.args?.query
|
|
95
|
+
const targets = Array.isArray(kase?.expect?.topNContains)
|
|
96
|
+
? kase.expect.topNContains.map((value) => String(value || '')).filter(Boolean)
|
|
97
|
+
: []
|
|
98
|
+
if (typeof query !== 'string' || !query.trim() || targets.length === 0) return null
|
|
99
|
+
const temporal = parsePeriod(String(kase?.args?.period || ''), true)
|
|
100
|
+
return {
|
|
101
|
+
id: String(kase.id),
|
|
102
|
+
caseFile,
|
|
103
|
+
label: String(kase.label || kase.id),
|
|
104
|
+
query: query.trim(),
|
|
105
|
+
language: classifyQueryLanguage(query),
|
|
106
|
+
targets,
|
|
107
|
+
contractTopN: Number.isInteger(kase?.expect?.topN) ? kase.expect.topN : 5,
|
|
108
|
+
filter: {
|
|
109
|
+
projectScope: String(kase?.args?.projectScope || 'mixdog'),
|
|
110
|
+
categories: (Array.isArray(kase?.args?.category) ? kase.args.category : [kase?.args?.category])
|
|
111
|
+
.map((value) => String(value || '').trim().toLowerCase())
|
|
112
|
+
.filter(Boolean),
|
|
113
|
+
startMs: Number.isFinite(Number(temporal?.startMs)) ? Number(temporal.startMs) : null,
|
|
114
|
+
endMs: Number.isFinite(Number(temporal?.endMs)) ? Number(temporal.endMs) : null,
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function documentMatchesFilter(document, filter = {}) {
|
|
120
|
+
const scope = String(filter.projectScope || 'mixdog')
|
|
121
|
+
if (scope === 'common' && document.projectId != null) return false
|
|
122
|
+
if (scope !== 'all' && scope !== 'common' && document.projectId != null && document.projectId !== scope) return false
|
|
123
|
+
if (Array.isArray(filter.categories) && filter.categories.length > 0) {
|
|
124
|
+
if (!filter.categories.includes(String(document.category || '').toLowerCase())) return false
|
|
125
|
+
}
|
|
126
|
+
const ts = Number(document.ts)
|
|
127
|
+
if (filter.startMs != null && (!Number.isFinite(ts) || ts < filter.startMs)) return false
|
|
128
|
+
if (filter.endMs != null && (!Number.isFinite(ts) || ts > filter.endMs)) return false
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function attachPositiveIds(evaluation, documents, targetMatches = new Map()) {
|
|
133
|
+
const eligible = documents.filter((document) => documentMatchesFilter(document, evaluation.filter))
|
|
134
|
+
const eligibleIds = new Set(eligible.map((document) => document.id))
|
|
135
|
+
const documentIds = new Set(documents.map((document) => document.id))
|
|
136
|
+
const positiveIdsByTarget = evaluation.targets.map((target) => {
|
|
137
|
+
const needle = target.toLowerCase()
|
|
138
|
+
const positiveIds = new Set(
|
|
139
|
+
eligible
|
|
140
|
+
.filter((document) => document.textLower.includes(needle))
|
|
141
|
+
.map((document) => document.id),
|
|
142
|
+
)
|
|
143
|
+
for (const match of targetMatches.get(needle) || []) {
|
|
144
|
+
if (!documentIds.has(match.rootId)) continue
|
|
145
|
+
if (!documentMatchesFilter(match, evaluation.filter)) continue
|
|
146
|
+
positiveIds.add(match.rootId)
|
|
147
|
+
}
|
|
148
|
+
return [...positiveIds]
|
|
149
|
+
})
|
|
150
|
+
const extraCandidateIds = [...new Set(positiveIdsByTarget.flat())]
|
|
151
|
+
.filter((id) => !eligibleIds.has(id))
|
|
152
|
+
return {
|
|
153
|
+
...evaluation,
|
|
154
|
+
candidateCount: eligible.length + extraCandidateIds.length,
|
|
155
|
+
extraCandidateIds,
|
|
156
|
+
positiveIdsByTarget,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function deterministicIds(ids) {
|
|
161
|
+
return [...new Set(ids)]
|
|
162
|
+
.map((id) => ({
|
|
163
|
+
id,
|
|
164
|
+
digest: createHash('sha256').update(String(id)).digest('hex'),
|
|
165
|
+
}))
|
|
166
|
+
.sort((left, right) => left.digest.localeCompare(right.digest) || left.id - right.id)
|
|
167
|
+
.map((row) => row.id)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function selectDeterministicCorpus(documents, evaluations, limit, positiveCap = 0) {
|
|
171
|
+
const requested = Number(limit)
|
|
172
|
+
const cap = Math.max(0, Math.floor(Number(positiveCap) || 0))
|
|
173
|
+
const targetPositiveIds = evaluations.flatMap((evaluation) => evaluation.positiveIdsByTarget)
|
|
174
|
+
const selectedPositiveIds = cap > 0
|
|
175
|
+
? targetPositiveIds.flatMap((ids) => deterministicIds(ids).slice(0, cap))
|
|
176
|
+
: targetPositiveIds.flat()
|
|
177
|
+
if (!Number.isInteger(requested) || requested <= 0 || requested >= documents.length) {
|
|
178
|
+
return {
|
|
179
|
+
documents,
|
|
180
|
+
sourceDocuments: documents.length,
|
|
181
|
+
selectedDocuments: documents.length,
|
|
182
|
+
positiveDocuments: new Set(selectedPositiveIds).size,
|
|
183
|
+
method: 'full-corpus',
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const positiveIds = new Set(selectedPositiveIds)
|
|
187
|
+
const remaining = documents
|
|
188
|
+
.filter((document) => !positiveIds.has(document.id))
|
|
189
|
+
.map((document) => ({
|
|
190
|
+
id: document.id,
|
|
191
|
+
digest: createHash('sha256').update(String(document.id)).digest('hex'),
|
|
192
|
+
}))
|
|
193
|
+
.sort((left, right) => left.digest.localeCompare(right.digest) || left.id - right.id)
|
|
194
|
+
const selectedIds = new Set(positiveIds)
|
|
195
|
+
const targetSize = Math.max(requested, selectedIds.size)
|
|
196
|
+
for (const row of remaining) {
|
|
197
|
+
if (selectedIds.size >= targetSize) break
|
|
198
|
+
selectedIds.add(row.id)
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
documents: documents.filter((document) => selectedIds.has(document.id)),
|
|
202
|
+
sourceDocuments: documents.length,
|
|
203
|
+
selectedDocuments: selectedIds.size,
|
|
204
|
+
positiveDocuments: positiveIds.size,
|
|
205
|
+
method: `${cap > 0 ? `up-to-${cap}-positive-roots-per-target` : 'all-positive-roots'} + sha256(document-id) negatives`,
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function tokenizeBm25(text) {
|
|
210
|
+
return String(text || '').toLowerCase().match(/[\p{L}\p{N}_-]+/gu) || []
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function prepareBm25Documents(documents) {
|
|
214
|
+
return documents.map((document) => {
|
|
215
|
+
const tokens = tokenizeBm25(document.text)
|
|
216
|
+
const termFrequency = new Map()
|
|
217
|
+
for (const token of tokens) termFrequency.set(token, (termFrequency.get(token) || 0) + 1)
|
|
218
|
+
return { length: tokens.length, termFrequency }
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function rankBm25(query, candidateIndices, preparedDocuments, k1 = 1.2, b = 0.75) {
|
|
223
|
+
const terms = [...new Set(tokenizeBm25(query))]
|
|
224
|
+
if (terms.length === 0 || candidateIndices.length === 0) return []
|
|
225
|
+
const averageLength = candidateIndices.reduce(
|
|
226
|
+
(sum, index) => sum + preparedDocuments[index].length,
|
|
227
|
+
0,
|
|
228
|
+
) / candidateIndices.length
|
|
229
|
+
const documentFrequency = new Map(terms.map((term) => [term, 0]))
|
|
230
|
+
for (const index of candidateIndices) {
|
|
231
|
+
const tf = preparedDocuments[index].termFrequency
|
|
232
|
+
for (const term of terms) if (tf.has(term)) documentFrequency.set(term, documentFrequency.get(term) + 1)
|
|
233
|
+
}
|
|
234
|
+
const scored = []
|
|
235
|
+
for (const index of candidateIndices) {
|
|
236
|
+
const document = preparedDocuments[index]
|
|
237
|
+
let score = 0
|
|
238
|
+
for (const term of terms) {
|
|
239
|
+
const frequency = document.termFrequency.get(term) || 0
|
|
240
|
+
if (frequency === 0) continue
|
|
241
|
+
const df = documentFrequency.get(term) || 0
|
|
242
|
+
const idf = Math.log(1 + ((candidateIndices.length - df + 0.5) / (df + 0.5)))
|
|
243
|
+
const denominator = frequency + k1 * (1 - b + b * (document.length / Math.max(1, averageLength)))
|
|
244
|
+
score += idf * ((frequency * (k1 + 1)) / denominator)
|
|
245
|
+
}
|
|
246
|
+
if (score > 0) scored.push({ index, score })
|
|
247
|
+
}
|
|
248
|
+
scored.sort((left, right) => right.score - left.score || left.index - right.index)
|
|
249
|
+
return scored
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function rankDense(queryVector, documentVectors, dims, candidateIndices) {
|
|
253
|
+
const scored = new Array(candidateIndices.length)
|
|
254
|
+
for (let position = 0; position < candidateIndices.length; position += 1) {
|
|
255
|
+
const index = candidateIndices[position]
|
|
256
|
+
const offset = index * dims
|
|
257
|
+
let score = 0
|
|
258
|
+
for (let dimension = 0; dimension < dims; dimension += 1) {
|
|
259
|
+
score += queryVector[dimension] * documentVectors[offset + dimension]
|
|
260
|
+
}
|
|
261
|
+
scored[position] = { index, score }
|
|
262
|
+
}
|
|
263
|
+
scored.sort((left, right) => right.score - left.score || left.index - right.index)
|
|
264
|
+
return scored
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function rankEqualRrf(denseRanked, lexicalRanked, k = 60) {
|
|
268
|
+
const scores = new Map()
|
|
269
|
+
for (let index = 0; index < denseRanked.length; index += 1) {
|
|
270
|
+
scores.set(denseRanked[index].index, 1 / (k + index + 1))
|
|
271
|
+
}
|
|
272
|
+
for (let index = 0; index < lexicalRanked.length; index += 1) {
|
|
273
|
+
const documentIndex = lexicalRanked[index].index
|
|
274
|
+
scores.set(documentIndex, (scores.get(documentIndex) || 0) + 1 / (k + index + 1))
|
|
275
|
+
}
|
|
276
|
+
return [...scores.entries()]
|
|
277
|
+
.map(([index, score]) => ({ index, score }))
|
|
278
|
+
.sort((left, right) => right.score - left.score || left.index - right.index)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function discountedGain(rank) {
|
|
282
|
+
return 1 / Math.log2(rank + 1)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function scoreRanking(evaluation, ranked, documents, cutoff = 10) {
|
|
286
|
+
const rankById = new Map()
|
|
287
|
+
for (let index = 0; index < ranked.length; index += 1) {
|
|
288
|
+
rankById.set(documents[ranked[index].index].id, index + 1)
|
|
289
|
+
}
|
|
290
|
+
const targetRows = evaluation.targets.map((target, index) => {
|
|
291
|
+
const positiveIds = evaluation.positiveIdsByTarget[index] || []
|
|
292
|
+
let rank = null
|
|
293
|
+
for (const id of positiveIds) {
|
|
294
|
+
const value = rankById.get(id)
|
|
295
|
+
if (value != null && (rank == null || value < rank)) rank = value
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
target,
|
|
299
|
+
positiveCount: positiveIds.length,
|
|
300
|
+
rank,
|
|
301
|
+
rrAt10: rank != null && rank <= cutoff ? 1 / rank : 0,
|
|
302
|
+
recallAt5: rank != null && rank <= 5 ? 1 : 0,
|
|
303
|
+
recallAt10: rank != null && rank <= cutoff ? 1 : 0,
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
const relevantIds = new Set(evaluation.positiveIdsByTarget.flat())
|
|
307
|
+
let dcg = 0
|
|
308
|
+
for (let index = 0; index < Math.min(cutoff, ranked.length); index += 1) {
|
|
309
|
+
if (relevantIds.has(documents[ranked[index].index].id)) dcg += discountedGain(index + 1)
|
|
310
|
+
}
|
|
311
|
+
let idealDcg = 0
|
|
312
|
+
for (let rank = 1; rank <= Math.min(cutoff, relevantIds.size); rank += 1) idealDcg += discountedGain(rank)
|
|
313
|
+
const divisor = targetRows.length || 1
|
|
314
|
+
return {
|
|
315
|
+
mrrAt10: targetRows.reduce((sum, row) => sum + row.rrAt10, 0) / divisor,
|
|
316
|
+
recallAt5: targetRows.reduce((sum, row) => sum + row.recallAt5, 0) / divisor,
|
|
317
|
+
recallAt10: targetRows.reduce((sum, row) => sum + row.recallAt10, 0) / divisor,
|
|
318
|
+
ndcgAt10: idealDcg > 0 ? dcg / idealDcg : 0,
|
|
319
|
+
targets: targetRows,
|
|
320
|
+
top3: ranked.slice(0, 3).map((row) => ({
|
|
321
|
+
id: documents[row.index].id,
|
|
322
|
+
score: row.score,
|
|
323
|
+
text: documents[row.index].text.slice(0, 240),
|
|
324
|
+
})),
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function mean(rows, key) {
|
|
329
|
+
if (rows.length === 0) return 0
|
|
330
|
+
return rows.reduce((sum, row) => sum + Number(row[key] || 0), 0) / rows.length
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function aggregateScoredRows(rows) {
|
|
334
|
+
const aggregate = (subset) => ({
|
|
335
|
+
cases: subset.length,
|
|
336
|
+
mrrAt10: mean(subset, 'mrrAt10'),
|
|
337
|
+
recallAt5: mean(subset, 'recallAt5'),
|
|
338
|
+
recallAt10: mean(subset, 'recallAt10'),
|
|
339
|
+
ndcgAt10: mean(subset, 'ndcgAt10'),
|
|
340
|
+
})
|
|
341
|
+
const byLanguage = {}
|
|
342
|
+
for (const language of ['ko', 'en', 'mixed', 'other']) {
|
|
343
|
+
byLanguage[language] = aggregate(rows.filter((row) => row.language === language))
|
|
344
|
+
}
|
|
345
|
+
return { overall: aggregate(rows), byLanguage }
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function percentile(values, ratio) {
|
|
349
|
+
if (!Array.isArray(values) || values.length === 0) return 0
|
|
350
|
+
const sorted = [...values].sort((left, right) => left - right)
|
|
351
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * ratio) - 1))
|
|
352
|
+
return sorted[index]
|
|
353
|
+
}
|