426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
473 lines
17 KiB
Python
Executable File
473 lines
17 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Append a new release entry to the voice-model version registry.
|
|
|
|
Inputs:
|
|
|
|
--id VoiceModelId — must be one of the values in
|
|
`packages/shared/src/local-inference/voice-models.ts`
|
|
(`speaker-encoder`, `diarizer`, `turn-detector`,
|
|
`voice-emotion`, `kokoro`, `omnivoice`, `vad`,
|
|
`wakeword`, `embedding`, `asr`).
|
|
--version Semver (e.g. `0.2.0`, `1.0.0-rc.3`).
|
|
--parent-version Predecessor version (omit for initial releases).
|
|
--hf-repo HuggingFace owner/repo holding the assets.
|
|
--hf-revision HF git revision (commit SHA or tag).
|
|
--asset Repeatable: `<filename>:<sha256>:<sizeBytes>:<quant>`
|
|
(one per published asset).
|
|
--min-bundle `eliza1Manifest.version` minimum compatibility.
|
|
--net-improvement Optional flag string `true` / `false`; defaults to
|
|
`true` for initial releases (no parent) and is
|
|
required otherwise.
|
|
--rtf-delta Optional eval delta vs parent (negative = faster).
|
|
--wer-delta Optional eval delta (negative = lower WER).
|
|
--eer-delta Optional eval delta (speaker-encoder; negative = lower EER).
|
|
--f1-delta Optional eval delta (positive = better F1).
|
|
--mos-delta Optional eval delta (positive = better MOS).
|
|
--false-bargein-delta Optional eval delta (VAD; negative = better).
|
|
--changelog-entry First line of the matching H3 block in
|
|
`models/voice/CHANGELOG.md` (also written to the
|
|
CHANGELOG when --append-changelog is set).
|
|
--voice-models-ts Path to the registry module — defaults to
|
|
`packages/shared/src/local-inference/voice-models.ts`.
|
|
--changelog-md Path to the human-readable changelog — defaults to
|
|
`models/voice/CHANGELOG.md`.
|
|
--append-changelog
|
|
Also append an H3 section to the CHANGELOG. The H3
|
|
is inserted directly under the H2 for the matching
|
|
id; reverse chronological per `models/voice/CHANGELOG.md`
|
|
convention.
|
|
--dry-run Print the planned changes; do not write to disk.
|
|
|
|
Idempotency: re-running with the same `--id` + `--version` returns
|
|
exit-code 0 without rewriting the file. Detection is exact-match on the
|
|
`{ id: "...", version: "..." }` literal; pre-release suffix differences
|
|
count as distinct versions.
|
|
|
|
The publish scripts (kokoro / omnivoice / etc.) should call this from
|
|
the final stage after `voice-preset.json` and `manifest-fragment.json`
|
|
are produced. The helper is intentionally separate from the publish
|
|
orchestrator so a reviewer can re-run it to backfill a missed entry
|
|
without re-running the entire publish.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import re
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
log = logging.getLogger("append_voice_model_version")
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
DEFAULT_VOICE_MODELS_TS = (
|
|
REPO_ROOT / "packages" / "shared" / "src" / "local-inference" / "voice-models.ts"
|
|
)
|
|
DEFAULT_CHANGELOG_MD = REPO_ROOT / "models" / "voice" / "CHANGELOG.md"
|
|
|
|
KNOWN_VOICE_MODEL_IDS = (
|
|
"speaker-encoder",
|
|
"diarizer",
|
|
"turn-detector",
|
|
"turn-detector-intl",
|
|
"voice-emotion",
|
|
"kokoro",
|
|
"omnivoice",
|
|
"vad",
|
|
"wakeword",
|
|
"embedding",
|
|
"asr",
|
|
)
|
|
|
|
KNOWN_QUANTS = (
|
|
"q4_0",
|
|
"q4_k_m",
|
|
"q5_k_m",
|
|
"q6_k",
|
|
"q8_0",
|
|
"fp16",
|
|
"onnx-fp16",
|
|
"onnx-int8",
|
|
)
|
|
|
|
SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$")
|
|
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
|
|
|
|
@dataclass
|
|
class Asset:
|
|
filename: str
|
|
sha256: str
|
|
size_bytes: int
|
|
quant: str
|
|
|
|
@classmethod
|
|
def parse(cls, raw: str) -> "Asset":
|
|
parts = raw.split(":")
|
|
if len(parts) != 4:
|
|
raise ValueError(
|
|
f"--asset must be `<filename>:<sha256>:<sizeBytes>:<quant>`; got {raw!r}"
|
|
)
|
|
filename, sha, size, quant = parts
|
|
if not SHA256_RE.match(sha):
|
|
raise ValueError(f"asset sha256 must be 64 lowercase hex: {sha!r}")
|
|
if not size.isdigit():
|
|
raise ValueError(f"asset sizeBytes must be a positive integer: {size!r}")
|
|
if quant not in KNOWN_QUANTS:
|
|
raise ValueError(
|
|
f"asset quant {quant!r} not in known set {KNOWN_QUANTS}"
|
|
)
|
|
if not filename:
|
|
raise ValueError("asset filename must be non-empty")
|
|
return cls(filename=filename, sha256=sha, size_bytes=int(size), quant=quant)
|
|
|
|
|
|
@dataclass
|
|
class VoiceVersion:
|
|
id: str
|
|
version: str
|
|
parent_version: str | None
|
|
published_at: str
|
|
hf_repo: str
|
|
hf_revision: str
|
|
assets: list[Asset]
|
|
eval_deltas: dict[str, object]
|
|
changelog_entry: str
|
|
min_bundle_version: str
|
|
|
|
|
|
def _format_asset_block(asset: Asset) -> str:
|
|
return (
|
|
" {\n"
|
|
f" filename: {json.dumps(asset.filename)},\n"
|
|
f" sha256: {json.dumps(asset.sha256)},\n"
|
|
f" sizeBytes: {asset.size_bytes},\n"
|
|
f" quant: {json.dumps(asset.quant)},\n"
|
|
" },"
|
|
)
|
|
|
|
|
|
def _format_eval_deltas(eval_deltas: dict[str, object]) -> str:
|
|
# Preserve insertion order so the resulting file is stable on re-runs.
|
|
keys = [
|
|
"rtfDelta",
|
|
"werDelta",
|
|
"eerDelta",
|
|
"f1Delta",
|
|
"mosDelta",
|
|
"falseBargeInDelta",
|
|
"netImprovement",
|
|
]
|
|
parts: list[str] = []
|
|
for k in keys:
|
|
if k in eval_deltas:
|
|
v = eval_deltas[k]
|
|
if isinstance(v, bool):
|
|
parts.append(f"{k}: {'true' if v else 'false'}")
|
|
elif isinstance(v, (int, float)):
|
|
parts.append(f"{k}: {v}")
|
|
else:
|
|
parts.append(f"{k}: {json.dumps(v)}")
|
|
return "{ " + ", ".join(parts) + " }"
|
|
|
|
|
|
def _format_version_block(v: VoiceVersion) -> str:
|
|
assets_text = (
|
|
"[]"
|
|
if not v.assets
|
|
else "[\n" + "\n".join(_format_asset_block(a) for a in v.assets) + "\n ]"
|
|
)
|
|
parent_line = (
|
|
f" parentVersion: {json.dumps(v.parent_version)},\n"
|
|
if v.parent_version is not None
|
|
else ""
|
|
)
|
|
return (
|
|
" {\n"
|
|
f" id: {json.dumps(v.id)},\n"
|
|
f" version: {json.dumps(v.version)},\n"
|
|
f"{parent_line}"
|
|
f" publishedToHfAt: {json.dumps(v.published_at)},\n"
|
|
f" hfRepo: {json.dumps(v.hf_repo)},\n"
|
|
f" hfRevision: {json.dumps(v.hf_revision)},\n"
|
|
f" ggufAssets: {assets_text},\n"
|
|
f" evalDeltas: {_format_eval_deltas(v.eval_deltas)},\n"
|
|
f" changelogEntry: {json.dumps(v.changelog_entry)},\n"
|
|
f" minBundleVersion: {json.dumps(v.min_bundle_version)},\n"
|
|
" },"
|
|
)
|
|
|
|
|
|
def already_has_entry(ts_text: str, model_id: str, version: str) -> bool:
|
|
"""Return True when `{ id: "<model_id>", version: "<version>", ... }`
|
|
already lives in `VOICE_MODEL_VERSIONS`. Match is structural and
|
|
tolerant of intervening lines / nested blocks (ggufAssets is a list
|
|
of inner `{}` objects, so we cannot rely on a flat `{...}` regex)."""
|
|
needle_id = re.compile(rf'\bid:\s*"{re.escape(model_id)}"')
|
|
needle_version = re.compile(rf'\bversion:\s*"{re.escape(version)}"')
|
|
# Walk every `id: "<model_id>"` occurrence and look forward for the
|
|
# matching `version: "<version>"` line within the same top-level
|
|
# VoiceVersion record. A record always begins with `id: "..."` and ends
|
|
# at the next sibling `},\n {` (or at the closing `];` of the array).
|
|
# Forward scan is bounded by the next top-level record terminator.
|
|
record_terminator = re.compile(r"\},\s*\{", re.MULTILINE)
|
|
end_of_array = re.compile(r"\}\s*,?\s*\];", re.MULTILINE)
|
|
for m in needle_id.finditer(ts_text):
|
|
scan_start = m.end()
|
|
terminator = record_terminator.search(ts_text, scan_start)
|
|
end = end_of_array.search(ts_text, scan_start)
|
|
if end is None and terminator is None:
|
|
continue
|
|
if end is None:
|
|
scan_end = terminator.start()
|
|
elif terminator is None:
|
|
scan_end = end.start()
|
|
else:
|
|
scan_end = min(terminator.start(), end.start())
|
|
if needle_version.search(ts_text, scan_start, scan_end):
|
|
return True
|
|
return False
|
|
|
|
|
|
def insert_into_voice_models_ts(
|
|
ts_text: str,
|
|
new_block: str,
|
|
) -> str:
|
|
"""Insert the new block at the top of `VOICE_MODEL_VERSIONS`."""
|
|
anchor = "export const VOICE_MODEL_VERSIONS: ReadonlyArray<VoiceModelVersion> = ["
|
|
idx = ts_text.find(anchor)
|
|
if idx == -1:
|
|
raise RuntimeError(
|
|
"VOICE_MODEL_VERSIONS anchor not found in voice-models.ts"
|
|
)
|
|
insert_at = idx + len(anchor)
|
|
# Walk past trailing whitespace and one newline so we land immediately
|
|
# before the first `{ id: …`.
|
|
while insert_at < len(ts_text) and ts_text[insert_at] in (" ", "\t", "\n"):
|
|
if ts_text[insert_at] == "\n":
|
|
insert_at += 1
|
|
break
|
|
insert_at += 1
|
|
return ts_text[:insert_at] + new_block + "\n" + ts_text[insert_at:]
|
|
|
|
|
|
def append_to_changelog(
|
|
md_text: str,
|
|
model_id: str,
|
|
version: str,
|
|
published_at: str,
|
|
parent_version: str | None,
|
|
hf_repo: str,
|
|
hf_revision: str,
|
|
changelog_entry: str,
|
|
eval_deltas: dict[str, object],
|
|
) -> str:
|
|
"""Insert an H3 block under the matching H2 in `CHANGELOG.md`."""
|
|
h2_re = re.compile(rf"(^##\s+{re.escape(model_id)}\s*$)", re.MULTILINE)
|
|
m = h2_re.search(md_text)
|
|
if m is None:
|
|
raise RuntimeError(
|
|
f"H2 for model id `{model_id}` not found in CHANGELOG.md; "
|
|
"add the H2 manually before re-running with --append-changelog"
|
|
)
|
|
insert_at = m.end()
|
|
# Match the per-id format from the existing CHANGELOG (see header).
|
|
date = published_at[:10]
|
|
parent_line = (
|
|
f"- **Parent:** {parent_version}." if parent_version else "- **Parent:** none."
|
|
)
|
|
net = eval_deltas.get("netImprovement", True)
|
|
deltas_lines: list[str] = []
|
|
for k, label in (
|
|
("rtfDelta", "RTF"),
|
|
("werDelta", "WER"),
|
|
("eerDelta", "EER"),
|
|
("f1Delta", "F1"),
|
|
("mosDelta", "MOS"),
|
|
("falseBargeInDelta", "False barge-in"),
|
|
):
|
|
if k in eval_deltas:
|
|
deltas_lines.append(f" - {label} Δ: {eval_deltas[k]}")
|
|
deltas_block = (
|
|
"- **Eval deltas:**\n" + "\n".join(deltas_lines)
|
|
if deltas_lines
|
|
else "- **Eval deltas:** (none recorded)"
|
|
)
|
|
block = (
|
|
f"\n\n### {version} — {date}\n\n"
|
|
f"- {changelog_entry}\n"
|
|
f"{parent_line}\n"
|
|
f"- **HF repo:** `{hf_repo}` @ rev `{hf_revision}`.\n"
|
|
f"{deltas_block}\n"
|
|
f"- **Net improvement:** {'yes' if net else 'no'}."
|
|
)
|
|
return md_text[:insert_at] + block + md_text[insert_at:]
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
p = argparse.ArgumentParser(
|
|
description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
p.add_argument("--id", required=True, choices=KNOWN_VOICE_MODEL_IDS)
|
|
p.add_argument("--version", required=True)
|
|
p.add_argument("--parent-version", default=None)
|
|
p.add_argument("--hf-repo", required=True)
|
|
p.add_argument("--hf-revision", required=True)
|
|
p.add_argument("--asset", action="append", default=[])
|
|
p.add_argument("--min-bundle", required=True)
|
|
p.add_argument("--net-improvement", default=None)
|
|
p.add_argument("--rtf-delta", type=float, default=None)
|
|
p.add_argument("--wer-delta", type=float, default=None)
|
|
p.add_argument("--eer-delta", type=float, default=None)
|
|
p.add_argument("--f1-delta", type=float, default=None)
|
|
p.add_argument("--mos-delta", type=float, default=None)
|
|
p.add_argument("--false-bargein-delta", type=float, default=None)
|
|
p.add_argument("--changelog-entry", required=True)
|
|
p.add_argument("--voice-models-ts", type=Path, default=DEFAULT_VOICE_MODELS_TS)
|
|
p.add_argument("--changelog-md", type=Path, default=DEFAULT_CHANGELOG_MD)
|
|
p.add_argument("--append-changelog", action="store_true")
|
|
p.add_argument("--dry-run", action="store_true")
|
|
return p
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = build_parser().parse_args(argv)
|
|
|
|
if not SEMVER_RE.match(args.version):
|
|
log.error("--version must be valid semver (got %s)", args.version)
|
|
return 2
|
|
if args.parent_version is not None and not SEMVER_RE.match(args.parent_version):
|
|
log.error(
|
|
"--parent-version must be valid semver (got %s)", args.parent_version
|
|
)
|
|
return 2
|
|
if not SEMVER_RE.match(args.min_bundle):
|
|
log.error("--min-bundle must be valid semver (got %s)", args.min_bundle)
|
|
return 2
|
|
|
|
assets = [Asset.parse(raw) for raw in args.asset]
|
|
if not assets:
|
|
log.warning(
|
|
"no --asset entries supplied; ggufAssets will be empty "
|
|
"(unpublished release seed)"
|
|
)
|
|
|
|
# netImprovement gate.
|
|
if args.net_improvement is not None:
|
|
if args.net_improvement.lower() not in {"true", "false"}:
|
|
log.error(
|
|
"--net-improvement must be `true` or `false`; got %r",
|
|
args.net_improvement,
|
|
)
|
|
return 2
|
|
net_improvement = args.net_improvement.lower() == "true"
|
|
else:
|
|
if args.parent_version is None:
|
|
net_improvement = True
|
|
else:
|
|
log.error(
|
|
"--net-improvement is required when --parent-version is set "
|
|
"(no implicit default for a successor release)"
|
|
)
|
|
return 2
|
|
|
|
eval_deltas: dict[str, object] = {}
|
|
if args.rtf_delta is not None:
|
|
eval_deltas["rtfDelta"] = args.rtf_delta
|
|
if args.wer_delta is not None:
|
|
eval_deltas["werDelta"] = args.wer_delta
|
|
if args.eer_delta is not None:
|
|
eval_deltas["eerDelta"] = args.eer_delta
|
|
if args.f1_delta is not None:
|
|
eval_deltas["f1Delta"] = args.f1_delta
|
|
if args.mos_delta is not None:
|
|
eval_deltas["mosDelta"] = args.mos_delta
|
|
if args.false_bargein_delta is not None:
|
|
eval_deltas["falseBargeInDelta"] = args.false_bargein_delta
|
|
eval_deltas["netImprovement"] = net_improvement
|
|
|
|
new_version = VoiceVersion(
|
|
id=args.id,
|
|
version=args.version,
|
|
parent_version=args.parent_version,
|
|
published_at=datetime.now(timezone.utc)
|
|
.replace(microsecond=0)
|
|
.isoformat()
|
|
.replace("+00:00", "Z"),
|
|
hf_repo=args.hf_repo,
|
|
hf_revision=args.hf_revision,
|
|
assets=assets,
|
|
eval_deltas=eval_deltas,
|
|
changelog_entry=args.changelog_entry,
|
|
min_bundle_version=args.min_bundle,
|
|
)
|
|
|
|
ts_path = args.voice_models_ts.resolve()
|
|
if not ts_path.exists():
|
|
log.error("voice-models.ts not found at %s", ts_path)
|
|
return 2
|
|
ts_text = ts_path.read_text(encoding="utf-8")
|
|
|
|
if already_has_entry(ts_text, args.id, args.version):
|
|
log.info(
|
|
"voice-models.ts already contains %s @ %s — unchanged (idempotent)",
|
|
args.id,
|
|
args.version,
|
|
)
|
|
return 0
|
|
|
|
new_block = _format_version_block(new_version)
|
|
updated_ts = insert_into_voice_models_ts(ts_text, new_block)
|
|
|
|
md_path = args.changelog_md.resolve()
|
|
updated_md: str | None = None
|
|
if args.append_changelog:
|
|
if not md_path.exists():
|
|
log.error(
|
|
"--append-changelog requested but CHANGELOG.md not at %s", md_path
|
|
)
|
|
return 2
|
|
md_text = md_path.read_text(encoding="utf-8")
|
|
# Detect an existing H3 with the same `### <version> ` heading.
|
|
if re.search(rf"^###\s+{re.escape(args.version)}\b", md_text, re.MULTILINE):
|
|
log.info(
|
|
"CHANGELOG.md already contains H3 for version %s — leaving file alone",
|
|
args.version,
|
|
)
|
|
else:
|
|
updated_md = append_to_changelog(
|
|
md_text=md_text,
|
|
model_id=args.id,
|
|
version=args.version,
|
|
published_at=new_version.published_at,
|
|
parent_version=args.parent_version,
|
|
hf_repo=args.hf_repo,
|
|
hf_revision=args.hf_revision,
|
|
changelog_entry=args.changelog_entry,
|
|
eval_deltas=eval_deltas,
|
|
)
|
|
|
|
if args.dry_run:
|
|
log.info("dry-run: would prepend new entry to %s", ts_path)
|
|
sys.stdout.write(new_block + "\n")
|
|
if updated_md is not None:
|
|
log.info("dry-run: would update %s", md_path)
|
|
return 0
|
|
|
|
ts_path.write_text(updated_ts, encoding="utf-8")
|
|
log.info("wrote %s — added %s @ %s", ts_path, args.id, args.version)
|
|
if updated_md is not None:
|
|
md_path.write_text(updated_md, encoding="utf-8")
|
|
log.info("wrote %s — appended H3 for %s @ %s", md_path, args.id, args.version)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|