mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-22 07:39:02 -04:00
* feat(ced): sketch sound-classification backend (CED audio tagger) Wires ced.cpp (CED, 527-class AudioSet sound-event tagger; baby cry, footsteps, glass, alarms, dog bark) into LocalAI as a Go/purego backend. SKETCH (backend skeleton real; core REST wiring + CI/gallery is a checklist in DESIGN.md): - backend/backend.proto: new SoundDetection rpc + SoundClass messages (run `make protogen-go` to regenerate pkg/grpc/proto). - backend/go/ced: main.go (purego dlopen libced.so + ced_capi.h), goced.go (Ced gRPC backend: Load + SoundDetection), Makefile (clone-at-pin CED_VERSION, ggml static-PIC shared build), run.sh, package.sh, .gitignore. - DESIGN.md: REST /v1/audio/classification wiring (handler/route/capability registration checklist), gallery/index + CI registration, and a scoping note for the realtime/websocket live-recognition path (sliding-window classify over the existing ws transport + voicegate; the ced C-API per-PCM entry point is already window-friendly). Backend code does not compile until protogen-go regenerates the pb types and a libced.so is built (Makefile clones+builds it). Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ced): REST /v1/audio/classification endpoint + capability registration Wires the ced sound-event classification backend (AudioSet audio tagger) end to end through the REST surface, mirroring the transcription path. - Handler: core/http/endpoints/openai/sound_classification.go parses the multipart audio upload, temp-files it, resolves the model config and calls the SoundDetection RPC; returns {model, detections[]} JSON. - Backend wrapper: core/backend/sound_classification.go (ModelSoundDetection) loads the model and normalizes the proto response into schema types. - Schema: core/schema/sound_classification.go (SoundClassificationResult). - gRPC layer: SoundDetection wired through the LocalAI wrapper (interface, Backend client, Client, embed, server, base default) so the loader-typed client exposes the RPC; proto regenerated via make protogen-go. - Route: POST /v1/audio/classification (+ /audio/classification alias) with the audio/multipart default-model middleware in routes/openai.go. - Capability surfaces: swagger @Tags/@Router on the handler; FLAG_SOUND_ CLASSIFICATION usecase flag + UsecaseSoundClassification + UsecaseInfoMap + GuessUsecases + ModalityGroups + GetAllModelConfigUsecases; meta usecase option; /api/instructions audio area updated; auth RouteFeatureRegistry + FeatureAudioClassification (APIFeatures, default ON) + FeatureMetas; UI usecaseFilters, capabilities.js CAP_SOUND_CLASSIFICATION, Models.jsx filter + i18n; docs page features/audio-classification.md + whats-new + crosslink. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ced): realtime sound-event detection over the websocket API When a realtime pipeline configures a sound-classification model, each VAD-committed utterance (the same window the transcription path produces) is also run through the CED sound-event classifier and the scored AudioSet tags are emitted as a new server event. No new backend rpc is needed: the SoundDetection gRPC method already exists on this branch. - config: add Pipeline.SoundDetection (yaml/json sound_detection,omitempty) beside Transcription/VAD. - realtime: add Model.SoundDetection(ctx, audio, topK, threshold) to the ModelInterface; implement it on wrappedModel and transcriptOnlyModel by calling backend.ModelSoundDetection with the session's sound-classification model config (mirrors how Transcribe dispatches). Load the optional config in newModel / newTranscriptionOnlyModel; nil config keeps it additive. - types: add ConversationItemSoundDetectionEvent (item_id, content_index, detections[]{label,score,index}) with type conversation.item.sound_detection, its ServerEventType constant and MarshalJSON, mirroring the transcription completed event. - realtime: add emitSoundDetection (unary path: classify the committed window, build the event, t.SendEvent) and wire it at the utterance-commit hook right after emitTranscription; gated on session.SoundDetectionEnabled (resolved from Pipeline.SoundDetection at session setup, defaults top_k=5, threshold=0). Its error is logged via xlog but never aborts the turn. - test: Ginkgo specs for emitSoundDetection (tags emitted, empty detections, classifier error) plus a SoundDetection method on the fakeModel double. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(ced): implement SoundDetection in nodes backend test doubles The SoundDetection method added to the grpc backend interface left two test doubles (fakeBackendClient, fakeGRPCBackend) incomplete, so core/services/nodes failed to compile under `go vet`/`go test` (go build missed it: the doubles live in _test.go). Add the method to both, mirroring their existing Detect mock. Repairs CI for the nodes package. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ced): decouple realtime sound detection from VAD (sound-only sessions) Sound-event detection must activate on sounds, not speech, so it no longer runs through the voice VAD/transcription path. A sound-detection-only pipeline (sound_detection set, no transcription/LLM) now: - is accepted by prepareRealtimeConfig (sound_detection counts as a pipeline stage), - builds a lightweight model via newSoundDetectionOnlyModel (no VAD/STT/LLM/TTS loaded), and - defaults the session to turn_detection none (no VAD) with no transcription stage, so the client drives windowing via input_audio_buffer.commit (option A: client-side sliding window). The per-PCM C-API already supports arbitrary windows. commitUtterance gains a sound-only branch: it emits the conversation.item.sound_detection event (scored AudioSet tags) and stops - no transcription, no LLM response. generateResponse is now guarded on a transcription stage being present, so a sound-only turn never invokes the LLM. Existing transcription/VAD sessions are unchanged (additive). Added a commitUtterance sound-only Ginkgo spec asserting it emits the sound event and neither transcribes nor generates a response. go vet + golangci-lint (new-from-merge-base) clean; openai suite green. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ced): register sound-classification backend in gallery + CI Mechanical backend-image registration for the ced sound-event classifier, mirroring the parakeet-cpp Go/purego backend everywhere it is wired up. - .github/backend-matrix.yml: add the ced build matrix, field-for-field copies of the parakeet-cpp entries (cpu amd64/arm64, cublas cuda 12/13 amd64, l4t cuda-13 arm64, l4t-jetpack cuda-12 arm64, sycl f32/f16, vulkan amd64/arm64, rocm hipblas, and the metal darwin entry), changing only backend and tag-suffix. dockerfile stays ./backend/Dockerfile.golang. - backend/index.yaml: add the &ced meta anchor (capabilities map per platform) plus ced-development and the per-arch image entries, each uri/mirror tag-suffix matching the matrix exactly. The model gallery (GGUF) entry is intentionally deferred pending the HuggingFace publish (TODO note inline). - scripts/changed-backends.js: add an explicit item.backend === "ced" branch in inferBackendPath mapping to backend/go/ced/, same mechanism and ordering as the parakeet-cpp branch (before the generic golang fallthrough). - .github/workflows/bump_deps.yaml: register mudler/ced.cpp -> CED_VERSION in backend/go/ced/Makefile so the daily bot bumps the pin. - swagger/{docs.go,swagger.json,swagger.yaml}: regenerated via make swagger so the existing /v1/audio/classification annotations land in the generated spec. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ced): server-side windowing for realtime sound detection (option B) Adds an optional server-driven sliding-window classifier so a sound-only realtime client only has to stream audio (no input_audio_buffer.commit): - Pipeline.sound_detection_window_ms / sound_detection_hop_ms config knobs. When both > 0 on a sound-only session, the server classifies the last window of streamed audio every hop and emits a conversation.item.sound_ detection event; the input buffer is trimmed to one window so a long stream stays bounded. When unset, the session stays client-driven (option A). Runs independent of VAD (sound events are not speech). - handleSoundWindow (ticker) + classifySoundWindow (one tick, extracted so it is unit-testable) + writeWindowWAV, which declares the true InputSampleRate (NewWAVHeaderWithRate) so the classifier resamples correctly. Goroutine is started after toggleVAD and torn down with the session (close + wg.Wait). - Register pipeline.sound_detection (+window_ms/hop_ms) in the config meta registry; the earlier realtime commit added pipeline.sound_detection without a registry entry, failing TestAllFieldsHaveRegistryEntries. This fixes that and covers the two new knobs. Tests: classifySoundWindow emits an event + trims the buffer to one window, no-ops on too-little audio; writeWindowWAV declares the given sample rate. go build/vet + golangci-lint (new-from-merge-base) clean; config + openai suites green. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ced): add ced-base GGUF model gallery entries (f16 + q8_0) The ced-base weights are now published at mudler/ced-base-gguf (Apache-2.0, converted from mispeech/ced-base). Adds gallery/ced.yaml (backend: ced + known_usecases: sound_classification) and two gallery/index.yaml entries (ced-base-f16 default, ced-base-q8 smallest) with sha256-pinned files, and removes the now-resolved TODO from backend/index.yaml. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ced): add tiny/mini/small GGUF model gallery entries Publishes the rest of the CED family (same architecture, metadata-driven port verified end-to-end on ced-tiny) to mudler/ced-{tiny,mini,small}-gguf and adds their f16 + q8_0 gallery entries: ced-tiny (5.5M, edge/Pi-class) f16 11MB / q8_0 6MB ced-mini (9.6M) f16 19MB / q8_0 11MB ced-small (22M) f16 42MB / q8_0 23MB All sha256-pinned. ced-base remains the accuracy default. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore(ced): point gallery entries at the consolidated mudler/ced-gguf repo All CED quantizations (tiny/mini/small/base, f16/q8_0) now live in a single HuggingFace repo, mudler/ced-gguf, instead of per-model repos. Repoint the 8 gallery model entries' urls + file uris accordingly. sha256 and filenames are unchanged. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore(ced): bump CED_VERSION to the short-clip fix Pin the ced backend to ced.cpp 99c6ed3, which fixes a crash on any clip shorter than target_length (~10.11s): time_pos_embed was added at its full 63-frame grid instead of being sliced to the clip's actual time grid, tripping ggml_can_repeat in ggml_add. Surfaced by the live realtime e2e (sub-10s windows) and gated with a short-clip parity test upstream. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * docs(ced): list ced.cpp as a LocalAI-team engine + backend-guide directive - README.md: add ced.cpp to the "native C/C++/GGML engines developed and maintained by the LocalAI project" table. - docs/content/features/backends.md: add a Sound Classification backend category (sound-event classification / audio tagging) listing ced.cpp. - .agents/adding-backends.md: add a "Documenting the backend" section and two verification-checklist items requiring new backends to be documented in the backends.md category list, and in-house native engines to be added to the README maintained-engines table. This directive was missing. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore(ced): repin CED_VERSION to the v0.1.0 release commit ced.cpp history was squashed into a single release commit (tagged v0.1.0), so the previous pin (99c6ed3) no longer exists upstream. Pin to c04ac14, the v0.1.0 release commit, so the backend builds against a commit that exists. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(ced): silence gosec G304/G103 + govet unsafeptr on audited paths - sound_classification.go: os.Create(dst) where dst = temp dir + path.Base of the upload (no traversal). #nosec G304, matching the depth-anything-cpp handler. - goced.go: reading a NUL-terminated C string from a libced-owned buffer. #nosec G103 (gosec) + //nolint:govet (golangci-lint's unsafeptr check), since the uintptr is a C-owned malloc'd buffer, not Go-GC memory. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
307 lines
13 KiB
JavaScript
307 lines
13 KiB
JavaScript
import fs from "fs";
|
|
import * as yaml from "js-yaml";
|
|
import { Octokit } from "@octokit/core";
|
|
|
|
// Matrix data lives in a small data-only YAML so both backend.yml (master push)
|
|
// and backend_pr.yml (pull_request) can use a dynamic `matrix: ${{ fromJson(...) }}`
|
|
// for the live job, while this script remains the single source of truth for
|
|
// "what backends does the project know about".
|
|
const matrixYml = yaml.load(fs.readFileSync(".github/backend-matrix.yml", "utf8"));
|
|
const includes = matrixYml.include;
|
|
const includesDarwin = matrixYml.includeDarwin;
|
|
|
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
const event = JSON.parse(fs.readFileSync(eventPath, "utf8"));
|
|
|
|
// Infer backend path
|
|
function inferBackendPath(item) {
|
|
if (item.dockerfile.endsWith("python")) {
|
|
return `backend/python/${item.backend}/`;
|
|
}
|
|
// parakeet-cpp is a Go backend (Dockerfile.golang) wrapping the parakeet.cpp
|
|
// ggml port via purego. It lives in backend/go/parakeet-cpp/; this explicit
|
|
// branch (placed before the generic golang one, which would also resolve it
|
|
// correctly) documents the mapping and guards against a future
|
|
// dockerfile-suffix change.
|
|
if (item.backend === "parakeet-cpp") {
|
|
return `backend/go/parakeet-cpp/`;
|
|
}
|
|
// ced is a Go backend (Dockerfile.golang) wrapping the ced.cpp ggml port via
|
|
// purego, living in backend/go/ced/. Same explicit-branch rationale as
|
|
// parakeet-cpp above: the generic golang fallthrough would also resolve it,
|
|
// but this documents the mapping and guards a future dockerfile-suffix change.
|
|
if (item.backend === "ced") {
|
|
return `backend/go/ced/`;
|
|
}
|
|
if (item.dockerfile.endsWith("golang")) {
|
|
return `backend/go/${item.backend}/`;
|
|
}
|
|
if (item.dockerfile.endsWith("rust")) {
|
|
return `backend/rust/${item.backend}/`;
|
|
}
|
|
if (item.dockerfile.endsWith("ik-llama-cpp")) {
|
|
return `backend/cpp/ik-llama-cpp/`;
|
|
}
|
|
if (item.dockerfile.endsWith("turboquant")) {
|
|
// turboquant is a llama.cpp fork that reuses backend/cpp/llama-cpp sources
|
|
// via a thin wrapper Makefile. Changes to either dir should retrigger it.
|
|
return `backend/cpp/turboquant/`;
|
|
}
|
|
if (item.dockerfile.endsWith("privacy-filter")) {
|
|
return `backend/cpp/privacy-filter/`;
|
|
}
|
|
if (item.dockerfile.endsWith("ds4")) {
|
|
return `backend/cpp/ds4/`;
|
|
}
|
|
if (item.dockerfile.endsWith("llama-cpp")) {
|
|
return `backend/cpp/llama-cpp/`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function inferBackendPathDarwin(item) {
|
|
// llama-cpp on Darwin builds from the C++ sources, not a backend/go/llama-cpp
|
|
// tree (which doesn't exist). The Darwin job is matrix-driven with lang=go
|
|
// for runner/toolchain selection, but the source path is C++.
|
|
if (item.backend === "llama-cpp") {
|
|
return `backend/cpp/llama-cpp/`;
|
|
}
|
|
// ds4 is C++ too (built via `make backends/ds4-darwin`); the matrix entry
|
|
// carries lang=go for runner/toolchain selection, but the source is C++.
|
|
if (item.backend === "ds4") {
|
|
return `backend/cpp/ds4/`;
|
|
}
|
|
if (!item.lang) {
|
|
return `backend/python/${item.backend}/`;
|
|
}
|
|
|
|
return `backend/${item.lang}/${item.backend}/`;
|
|
}
|
|
|
|
// Build a deduplicated map of backend name -> path prefix from all matrix entries
|
|
function getAllBackendPaths() {
|
|
const paths = new Map();
|
|
for (const item of includes) {
|
|
const p = inferBackendPath(item);
|
|
if (p && !paths.has(item.backend)) {
|
|
paths.set(item.backend, p);
|
|
}
|
|
}
|
|
for (const item of includesDarwin) {
|
|
const p = inferBackendPathDarwin(item);
|
|
if (p && !paths.has(item.backend)) {
|
|
paths.set(item.backend, p);
|
|
}
|
|
}
|
|
return paths;
|
|
}
|
|
|
|
const allBackendPaths = getAllBackendPaths();
|
|
|
|
const token = process.env.GITHUB_TOKEN;
|
|
const octokit = new Octokit({ auth: token });
|
|
|
|
// PR file list — paginated.
|
|
async function getChangedFilesForPR(event) {
|
|
const prNumber = event.pull_request.number;
|
|
const repo = event.repository.name;
|
|
const owner = event.repository.owner.login;
|
|
let files = [];
|
|
let page = 1;
|
|
while (true) {
|
|
const res = await octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}/files', {
|
|
owner,
|
|
repo,
|
|
pull_number: prNumber,
|
|
per_page: 100,
|
|
page
|
|
});
|
|
files = files.concat(res.data.map(f => f.filename));
|
|
if (res.data.length < 100) break;
|
|
page++;
|
|
}
|
|
return files;
|
|
}
|
|
|
|
// Branch-push file list — uses the Compare API so it works in shallow clones.
|
|
// Returns null to signal "we cannot compute a reliable diff; run everything".
|
|
async function getChangedFilesForPush(event) {
|
|
const before = event.before;
|
|
const after = event.after;
|
|
// First push to a branch carries an all-zero `before` SHA and there's no
|
|
// base to diff against. Run everything in that case.
|
|
if (!before || !after || /^0+$/.test(before)) return null;
|
|
const owner = event.repository.owner.login;
|
|
const repo = event.repository.name;
|
|
let res;
|
|
try {
|
|
res = await octokit.request('GET /repos/{owner}/{repo}/compare/{basehead}', {
|
|
owner,
|
|
repo,
|
|
basehead: `${before}...${after}`,
|
|
});
|
|
} catch (err) {
|
|
console.log("compare API failed, falling back to run-all:", err.message);
|
|
return null;
|
|
}
|
|
if (!res.data || !Array.isArray(res.data.files)) return null;
|
|
// The compare endpoint caps the file list at 300. If we hit the cap we may
|
|
// be missing changes — be conservative and run everything.
|
|
if (res.data.files.length >= 300) {
|
|
console.log("compare API returned 300+ files (truncated), falling back to run-all");
|
|
return null;
|
|
}
|
|
return res.data.files.map(f => f.filename);
|
|
}
|
|
|
|
// Group matrix entries by tag-suffix and emit a merge-matrix entry per group.
|
|
// Both multi-leg groups (per-arch fan-out) and singletons get one entry each:
|
|
// the build job pushes by digest only with no tags applied, so every backend
|
|
// needs a downstream merge step to apply its tags via `imagetools create`,
|
|
// regardless of how many per-arch legs feed it. Callers split entries by
|
|
// arch class first (see splitByArch) and call this once per class so the
|
|
// resulting matrices can be wired to merge jobs that `needs:` only their
|
|
// corresponding build matrix — preventing slow single-arch builds from
|
|
// gating multi-arch merges (the bug fixed in PR #9746).
|
|
function computeMergeMatrix(entries) {
|
|
const groups = new Map();
|
|
for (const item of entries) {
|
|
if (!item['tag-suffix']) continue;
|
|
const key = item['tag-suffix'];
|
|
if (!groups.has(key)) groups.set(key, []);
|
|
groups.get(key).push(item);
|
|
}
|
|
const include = [];
|
|
for (const [tagSuffix, group] of groups) {
|
|
// tag-latest must agree across legs — they're going to publish under
|
|
// the same final tag, so disagreeing on whether it's also the :latest
|
|
// tag is an authoring bug. Warn loudly so a Task 2.5 fan-out typo is
|
|
// visible in CI logs instead of silently shipping the leg-0 value.
|
|
const first = group[0]['tag-latest'] || '';
|
|
for (const m of group) {
|
|
if ((m['tag-latest'] || '') !== first) {
|
|
console.warn(`tag-latest mismatch in group ${tagSuffix}: legs disagree (using ${first})`);
|
|
break;
|
|
}
|
|
}
|
|
include.push({
|
|
'tag-suffix': tagSuffix,
|
|
'tag-latest': first,
|
|
});
|
|
}
|
|
return { include };
|
|
}
|
|
|
|
// Split a list of linux matrix entries into single-arch (no platform-tag) and
|
|
// multi-arch (platform-tag set, paired with a sibling entry sharing the same
|
|
// tag-suffix). The two are run as separate matrix jobs so backend-merge-jobs
|
|
// can `needs:` only the multi-arch one — slow single-arch builds (CUDA, ROCm,
|
|
// vLLM, etc.) don't block manifest assembly while their per-arch counterparts'
|
|
// untagged digests sit on quay long enough to be GC'd.
|
|
function splitByArch(entries) {
|
|
const multiarch = entries.filter(e => e['platform-tag']);
|
|
const singlearch = entries.filter(e => !e['platform-tag']);
|
|
return { multiarch, singlearch };
|
|
}
|
|
|
|
function emitFullMatrix() {
|
|
const { multiarch, singlearch } = splitByArch(includes);
|
|
const mergeMatrixMultiarch = computeMergeMatrix(multiarch);
|
|
const mergeMatrixSinglearch = computeMergeMatrix(singlearch);
|
|
const hasMergesMultiarch = mergeMatrixMultiarch.include.length > 0 ? 'true' : 'false';
|
|
const hasMergesSinglearch = mergeMatrixSinglearch.include.length > 0 ? 'true' : 'false';
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `run-all=true\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-singlearch=${singlearch.length > 0 ? 'true' : 'false'}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-multiarch=${multiarch.length > 0 ? 'true' : 'false'}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-darwin=true\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-merges-multiarch=${hasMergesMultiarch}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-merges-singlearch=${hasMergesSinglearch}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-singlearch=${JSON.stringify({ include: singlearch })}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-multiarch=${JSON.stringify({ include: multiarch })}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-darwin=${JSON.stringify({ include: includesDarwin })}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `merge-matrix-multiarch=${JSON.stringify(mergeMatrixMultiarch)}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `merge-matrix-singlearch=${JSON.stringify(mergeMatrixSinglearch)}\n`);
|
|
for (const backend of allBackendPaths.keys()) {
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${backend}=true\n`);
|
|
}
|
|
}
|
|
|
|
function emitFilteredMatrix(changedFiles) {
|
|
console.log("Changed files:", changedFiles);
|
|
|
|
const filtered = includes.filter(item => {
|
|
const backendPath = inferBackendPath(item);
|
|
if (!backendPath) return false;
|
|
return changedFiles.some(file => file.startsWith(backendPath));
|
|
});
|
|
|
|
const filteredDarwin = includesDarwin.filter(item => {
|
|
const backendPath = inferBackendPathDarwin(item);
|
|
return changedFiles.some(file => file.startsWith(backendPath));
|
|
});
|
|
|
|
console.log("Filtered files:", filtered);
|
|
console.log("Filtered files Darwin:", filteredDarwin);
|
|
|
|
const { multiarch, singlearch } = splitByArch(filtered);
|
|
const hasBackendsSinglearch = singlearch.length > 0 ? 'true' : 'false';
|
|
const hasBackendsMultiarch = multiarch.length > 0 ? 'true' : 'false';
|
|
const hasBackendsDarwin = filteredDarwin.length > 0 ? 'true' : 'false';
|
|
console.log("Has single-arch backends?:", hasBackendsSinglearch);
|
|
console.log("Has multi-arch backends?:", hasBackendsMultiarch);
|
|
console.log("Has Darwin backends?:", hasBackendsDarwin);
|
|
|
|
const mergeMatrixMultiarch = computeMergeMatrix(multiarch);
|
|
const mergeMatrixSinglearch = computeMergeMatrix(singlearch);
|
|
const hasMergesMultiarch = mergeMatrixMultiarch.include.length > 0 ? 'true' : 'false';
|
|
const hasMergesSinglearch = mergeMatrixSinglearch.include.length > 0 ? 'true' : 'false';
|
|
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `run-all=false\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-singlearch=${hasBackendsSinglearch}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-multiarch=${hasBackendsMultiarch}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-darwin=${hasBackendsDarwin}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-merges-multiarch=${hasMergesMultiarch}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-merges-singlearch=${hasMergesSinglearch}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-singlearch=${JSON.stringify({ include: singlearch })}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-multiarch=${JSON.stringify({ include: multiarch })}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-darwin=${JSON.stringify({ include: filteredDarwin })}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `merge-matrix-multiarch=${JSON.stringify(mergeMatrixMultiarch)}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `merge-matrix-singlearch=${JSON.stringify(mergeMatrixSinglearch)}\n`);
|
|
|
|
// Per-backend boolean outputs
|
|
for (const [backend, pathPrefix] of allBackendPaths) {
|
|
let changed = changedFiles.some(file => file.startsWith(pathPrefix));
|
|
// turboquant reuses backend/cpp/llama-cpp sources via a thin wrapper;
|
|
// changes to either directory should retrigger its pipeline.
|
|
if (backend === "turboquant" && !changed) {
|
|
changed = changedFiles.some(file => file.startsWith("backend/cpp/llama-cpp/"));
|
|
}
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${backend}=${changed ? 'true' : 'false'}\n`);
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
// Tag pushes and an explicit FORCE_ALL escape hatch always rebuild everything.
|
|
// FORCE_ALL is set from backend.yml whenever github.ref starts with refs/tags/.
|
|
const forceAll = process.env.FORCE_ALL === 'true';
|
|
const isTagPush = typeof event.ref === 'string' && event.ref.startsWith('refs/tags/');
|
|
const isBranchPush = !!event.ref && !event.pull_request && !isTagPush;
|
|
|
|
let changedFiles = null;
|
|
if (event.pull_request) {
|
|
changedFiles = await getChangedFilesForPR(event);
|
|
} else if (isBranchPush && !forceAll) {
|
|
changedFiles = await getChangedFilesForPush(event);
|
|
// null -> fall through to the full matrix (e.g. first push, API truncated,
|
|
// network failure).
|
|
}
|
|
// All other event types (workflow_dispatch, schedule, tag pushes, FORCE_ALL)
|
|
// leave changedFiles === null and run everything.
|
|
|
|
if (changedFiles === null) {
|
|
emitFullMatrix();
|
|
return;
|
|
}
|
|
emitFilteredMatrix(changedFiles);
|
|
})();
|