Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
290 lines
15 KiB
Python
290 lines
15 KiB
Python
#!/usr/bin/env python3
|
||
"""Build the strict, hash-verified acceptance package for Experiment 8-6."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import hashlib
|
||
import itertools
|
||
import json
|
||
from pathlib import Path
|
||
|
||
import librosa
|
||
import numpy as np
|
||
import soundfile as sf
|
||
import torch
|
||
from huggingface_hub import hf_hub_download
|
||
from transformers import AutoFeatureExtractor, AutoModelForAudioClassification
|
||
|
||
AST_MODEL = "MIT/ast-finetuned-audioset-10-10-0.4593"
|
||
|
||
|
||
def sha256(path: Path) -> str:
|
||
h = hashlib.sha256()
|
||
with path.open("rb") as f:
|
||
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
||
h.update(chunk)
|
||
return h.hexdigest()
|
||
|
||
|
||
def audio_stats(path: Path):
|
||
y, sr = sf.read(path, dtype="float32")
|
||
if y.ndim > 1:
|
||
y = y.mean(axis=1)
|
||
rms = float(np.sqrt(np.mean(np.square(y)))) if len(y) else 0.0
|
||
mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=20)
|
||
# Mean + variability is a transparent timbre proxy, not a human quality score.
|
||
embedding = np.concatenate([mfcc.mean(axis=1), mfcc.std(axis=1)])
|
||
embedding /= max(float(np.linalg.norm(embedding)), 1e-12)
|
||
return {
|
||
"samples": len(y),
|
||
"sample_rate": sr,
|
||
"seconds": len(y) / sr,
|
||
"rms": rms,
|
||
"embedding": embedding,
|
||
}
|
||
|
||
|
||
def orpheus_analysis(root: Path, manifest):
|
||
result = {}
|
||
failures = []
|
||
for arm in ("base", "adapted"):
|
||
records = [x for x in manifest["audio"] if f"orpheus/{arm}/" in x["path"]]
|
||
stats = []
|
||
for rec in records:
|
||
item = audio_stats(root / rec["path"])
|
||
item.update({"prompt_id": rec["prompt_id"], "path": rec["path"]})
|
||
stats.append(item)
|
||
if item["samples"] < 2400 or item["rms"] < 1e-5:
|
||
failures.append({"track": "orpheus", "arm": arm, "reason": "short_or_silent", **{k: v for k, v in item.items() if k != "embedding"}})
|
||
similarities = []
|
||
for a, b in itertools.combinations(stats, 2):
|
||
similarities.append(
|
||
{
|
||
"prompt_a": a["prompt_id"],
|
||
"prompt_b": b["prompt_id"],
|
||
"cosine": float(np.dot(a["embedding"], b["embedding"])),
|
||
}
|
||
)
|
||
similarities.sort(key=lambda x: x["cosine"])
|
||
if similarities:
|
||
failures.append({"track": "orpheus", "arm": arm, "reason": "lowest_cross_sentence_timbre_proxy", **similarities[0]})
|
||
result[arm] = {
|
||
"audio_count": len(stats),
|
||
"valid_audio_count": sum(x["samples"] >= 2400 and x["rms"] >= 1e-5 for x in stats),
|
||
"mean_pairwise_mfcc_cosine": float(np.mean([x["cosine"] for x in similarities])),
|
||
"min_pairwise_mfcc_cosine": min((x["cosine"] for x in similarities), default=None),
|
||
"pairwise": similarities,
|
||
}
|
||
result["adapted_minus_base_mean_pairwise_mfcc_cosine"] = (
|
||
result["adapted"]["mean_pairwise_mfcc_cosine"] - result["base"]["mean_pairwise_mfcc_cosine"]
|
||
)
|
||
return result, failures
|
||
|
||
|
||
def find_label(model, needle):
|
||
labels = model.config.id2label
|
||
matches = [int(i) for i, label in labels.items() if needle.lower() == label.lower()]
|
||
if not matches:
|
||
matches = [int(i) for i, label in labels.items() if needle.lower() in label.lower()]
|
||
if not matches:
|
||
raise RuntimeError(f"AudioSet label not found: {needle}")
|
||
return matches[0], labels[matches[0]]
|
||
|
||
|
||
def sesame_analysis(root: Path, manifest):
|
||
extractor = AutoFeatureExtractor.from_pretrained(AST_MODEL)
|
||
model = AutoModelForAudioClassification.from_pretrained(AST_MODEL).cuda().eval()
|
||
label_ids = {}
|
||
for tag, needle in {"laugh": "Laughter", "giggle": "Giggle", "sigh": "Sigh"}.items():
|
||
label_ids[tag] = find_label(model, needle)
|
||
scores = []
|
||
failures = []
|
||
for rec in manifest["audio"]:
|
||
path = root / rec["path"]
|
||
y, sr = librosa.load(path, sr=16000, mono=True)
|
||
rms = float(np.sqrt(np.mean(np.square(y)))) if len(y) else 0.0
|
||
inputs = extractor(y, sampling_rate=16000, return_tensors="pt").to("cuda")
|
||
with torch.inference_mode():
|
||
probs = model(**inputs).logits.sigmoid()[0]
|
||
label_id, label_name = label_ids[rec["tag"]]
|
||
row = {
|
||
"arm": "adapted" if "/adapted/" in rec["path"] else "base",
|
||
"pair_id": rec["pair_id"],
|
||
"condition": rec["condition"],
|
||
"tag": rec["tag"],
|
||
"audioset_label": label_name,
|
||
"audioset_score": float(probs[label_id].cpu()),
|
||
"seconds": len(y) / 16000,
|
||
"rms": rms,
|
||
"path": rec["path"],
|
||
}
|
||
scores.append(row)
|
||
if len(y) < 1600 or rms < 1e-5:
|
||
failures.append({"track": "sesame", "reason": "short_or_silent", **row})
|
||
arms = {}
|
||
for arm in ("base", "adapted"):
|
||
pairs = []
|
||
for pair_id in sorted({x["pair_id"] for x in scores if x["arm"] == arm}):
|
||
neutral = next(x for x in scores if x["arm"] == arm and x["pair_id"] == pair_id and x["condition"] == "neutral")
|
||
tagged = next(x for x in scores if x["arm"] == arm and x["pair_id"] == pair_id and x["condition"] == "tagged")
|
||
pair = {
|
||
"pair_id": pair_id,
|
||
"tag": tagged["tag"],
|
||
"neutral_score": neutral["audioset_score"],
|
||
"tagged_score": tagged["audioset_score"],
|
||
"tagged_minus_neutral": tagged["audioset_score"] - neutral["audioset_score"],
|
||
}
|
||
pairs.append(pair)
|
||
if pair["tagged_minus_neutral"] <= 0:
|
||
failures.append({"track": "sesame", "arm": arm, "reason": "tag_did_not_raise_matching_audioset_score", **pair})
|
||
arms[arm] = {
|
||
"audio_count": sum(x["arm"] == arm for x in scores),
|
||
"valid_audio_count": sum(x["arm"] == arm and x["seconds"] >= 0.1 and x["rms"] >= 1e-5 for x in scores),
|
||
"mean_tagged_minus_neutral": float(np.mean([x["tagged_minus_neutral"] for x in pairs])),
|
||
"positive_pair_count": sum(x["tagged_minus_neutral"] > 0 for x in pairs),
|
||
"pairs": pairs,
|
||
}
|
||
result = {
|
||
"classifier": AST_MODEL,
|
||
"labels": {k: {"id": v[0], "name": v[1]} for k, v in label_ids.items()},
|
||
"base": arms["base"],
|
||
"adapted": arms["adapted"],
|
||
"adapted_minus_base_mean_tag_sensitivity": arms["adapted"]["mean_tagged_minus_neutral"] - arms["base"]["mean_tagged_minus_neutral"],
|
||
"scores": scores,
|
||
}
|
||
return result, failures
|
||
|
||
|
||
def verify_remote_adapter(manifest):
|
||
expected = next(
|
||
x for x in manifest["adapter_local_files"] if x["path"].endswith("adapter_model.safetensors")
|
||
)
|
||
repo_id = manifest["adapter_huggingface_repo"].removeprefix("https://huggingface.co/")
|
||
downloaded = Path(
|
||
hf_hub_download(
|
||
repo_id,
|
||
"adapter_model.safetensors",
|
||
revision=manifest["adapter_huggingface_revision"],
|
||
)
|
||
)
|
||
actual = sha256(downloaded)
|
||
return {
|
||
"repository": manifest["adapter_huggingface_repo"],
|
||
"revision": manifest["adapter_huggingface_revision"],
|
||
"expected_sha256": expected["sha256"],
|
||
"downloaded_sha256": actual,
|
||
"verified": actual == expected["sha256"],
|
||
}
|
||
|
||
|
||
def main():
|
||
p = argparse.ArgumentParser()
|
||
p.add_argument("--run", type=Path, required=True)
|
||
args = p.parse_args()
|
||
root = args.run
|
||
orpheus_manifest = json.loads((root / "orpheus_manifest.json").read_text(encoding="utf-8"))
|
||
sesame_manifest = json.loads((root / "sesame_manifest.json").read_text(encoding="utf-8"))
|
||
orpheus, orpheus_failures = orpheus_analysis(root, orpheus_manifest)
|
||
sesame, sesame_failures = sesame_analysis(root, sesame_manifest)
|
||
adapter_verification = {
|
||
"orpheus": verify_remote_adapter(orpheus_manifest),
|
||
"sesame": verify_remote_adapter(sesame_manifest),
|
||
}
|
||
|
||
gates = {
|
||
"orpheus_128_train_examples": orpheus_manifest["train_examples_encoded"] >= 128,
|
||
"orpheus_16_held_out_examples": orpheus_manifest["eval_examples_encoded"] >= 16,
|
||
"orpheus_60_optimizer_steps": orpheus_manifest["optimizer_steps"] >= 60,
|
||
"orpheus_remote_adapter_sha256_verified": adapter_verification["orpheus"]["verified"],
|
||
"orpheus_16_valid_comparison_files": orpheus["base"]["valid_audio_count"] == 8 and orpheus["adapted"]["valid_audio_count"] == 8,
|
||
"sesame_128_train_examples": sesame_manifest["train_examples_preprocessed"] >= 128,
|
||
"sesame_tag_categories_present": all(sesame_manifest["train_category_counts"].get(x, 0) > 0 for x in ("laugh", "giggle", "sigh", "neutral")),
|
||
"sesame_60_optimizer_steps": sesame_manifest["optimizer_steps"] >= 60,
|
||
"sesame_remote_adapter_sha256_verified": adapter_verification["sesame"]["verified"],
|
||
"sesame_24_valid_comparison_files": sesame["base"]["valid_audio_count"] == 12 and sesame["adapted"]["valid_audio_count"] == 12,
|
||
}
|
||
hypotheses = {
|
||
"orpheus_held_out_loss_decreased": orpheus_manifest["post_eval"]["eval_loss"] < orpheus_manifest["pre_eval"]["eval_loss"],
|
||
"orpheus_cross_sentence_timbre_proxy_improved": orpheus["adapted_minus_base_mean_pairwise_mfcc_cosine"] > 0,
|
||
"sesame_held_out_loss_decreased": sesame_manifest["post_eval"]["eval_loss"] < sesame_manifest["pre_eval"]["eval_loss"],
|
||
"sesame_adapted_mean_tag_score_is_positive": sesame["adapted"]["mean_tagged_minus_neutral"] > 0,
|
||
"sesame_tag_sensitivity_improved_over_base": sesame["adapted_minus_base_mean_tag_sensitivity"] > 0,
|
||
}
|
||
analysis = {
|
||
"experiment": "8-6",
|
||
"execution_acceptance": "PASS" if all(gates.values()) else "FAIL",
|
||
"execution_gates": gates,
|
||
"hypothesis_results": hypotheses,
|
||
"quality_claim": "No human naturalness or voice-identity quality claim; automatic metrics are reproducible proxies only.",
|
||
"remote_adapter_verification": adapter_verification,
|
||
"orpheus": orpheus,
|
||
"sesame": sesame,
|
||
}
|
||
(root / "analysis.json").write_text(json.dumps(analysis, indent=2) + "\n", encoding="utf-8")
|
||
failures = orpheus_failures + sesame_failures
|
||
(root / "failure_comparisons.json").write_text(json.dumps(failures, indent=2) + "\n", encoding="utf-8")
|
||
|
||
inventory = []
|
||
external_blob_names = {"adapter_model.safetensors", "tokenizer.json", "tokenizer_config.json"}
|
||
for path in sorted(root.rglob("*")):
|
||
if path.is_file() and path.name not in {"artifact_inventory.json", "REPORT.md"} | external_blob_names:
|
||
inventory.append({"path": str(path.relative_to(root)), "bytes": path.stat().st_size, "sha256": sha256(path)})
|
||
for manifest in (orpheus_manifest, sesame_manifest):
|
||
for item in manifest["adapter_local_files"]:
|
||
if Path(item["path"]).name in external_blob_names:
|
||
inventory.append({
|
||
**item,
|
||
"storage": "huggingface",
|
||
"repository": manifest["adapter_huggingface_repo"],
|
||
"revision": manifest["adapter_huggingface_revision"],
|
||
})
|
||
(root / "artifact_inventory.json").write_text(json.dumps(inventory, indent=2) + "\n", encoding="utf-8")
|
||
|
||
gate_lines = "\n".join(f"- {'PASS' if ok else 'FAIL'} — `{name}`" for name, ok in gates.items())
|
||
hypothesis_lines = "\n".join(f"- {'SUPPORTED' if ok else 'NOT SUPPORTED'} — `{name}`" for name, ok in hypotheses.items())
|
||
report = f"""# Experiment 8-6 strict acceptance report
|
||
|
||
Execution acceptance: **{analysis['execution_acceptance']}**
|
||
|
||
This run trained two real LoRA adapters on an RTX PRO 6000. It used 128 Orpheus training utterances plus 16 held-out utterances, and {sesame_manifest['train_examples_preprocessed']} stratified Sesame training utterances plus {sesame_manifest['eval_examples_preprocessed']} held-out utterances. Each track completed 60 optimizer updates at effective batch size four. Both adapters are identified by local SHA-256 inventories and public Hugging Face repositories.
|
||
|
||
## Execution gates
|
||
|
||
{gate_lines}
|
||
|
||
## Hypothesis results
|
||
|
||
{hypothesis_lines}
|
||
|
||
Execution completion and hypothesis support are intentionally separate. A completed campaign may produce a negative hypothesis result.
|
||
|
||
## Orpheus result
|
||
|
||
- Held-out loss: {orpheus_manifest['pre_eval']['eval_loss']:.6f} before → {orpheus_manifest['post_eval']['eval_loss']:.6f} after.
|
||
- Mean cross-sentence MFCC-statistic cosine: {orpheus['base']['mean_pairwise_mfcc_cosine']:.6f} base → {orpheus['adapted']['mean_pairwise_mfcc_cosine']:.6f} adapted (Δ {orpheus['adapted_minus_base_mean_pairwise_mfcc_cosine']:+.6f}).
|
||
- Eight unseen sentences were generated for each arm with matched seeds. This metric is a timbre-consistency proxy; it is not speaker-verification or a listening-test score.
|
||
|
||
## Sesame result
|
||
|
||
- Held-out loss: {sesame_manifest['pre_eval']['eval_loss']:.6f} before → {sesame_manifest['post_eval']['eval_loss']:.6f} after.
|
||
- Mean matching AudioSet event-score difference (tagged − neutral): {sesame['base']['mean_tagged_minus_neutral']:+.6f} base → {sesame['adapted']['mean_tagged_minus_neutral']:+.6f} adapted (Δ {sesame['adapted_minus_base_mean_tag_sensitivity']:+.6f}).
|
||
- Positive matched pairs: {sesame['base']['positive_pair_count']}/6 base; {sesame['adapted']['positive_pair_count']}/6 adapted.
|
||
- Six prompt pairs (laugh, giggle, sigh) were generated per arm with the same seed within each tagged/neutral pair. AudioSet scores are detector proxies, not proof of natural expression.
|
||
|
||
## Failure retention and limits
|
||
|
||
`failure_comparisons.json` retains silent/short outputs, each Orpheus arm's least-consistent sentence pair, and every Sesame pair where adding a tag did not raise the matching AudioSet score. `compatibility_failures.json` retains the disabled-source-dataset failure, current Unsloth CSM pad-token rejection, and Transformers bf16 codec merge failure, together with the exact standard-PEFT/float32 fallback. The Sesame held-out loss split contains laugh, sigh, and neutral examples but no giggle examples because all 32 available giggle-tagged rows were allocated to the substantive training split. The campaign does not include blinded human MOS, speaker-verification enrollment, confidence intervals over multiple training seeds, or deployment-scale data. Therefore it makes no claim of perceptual quality or generalization beyond this bounded run.
|
||
|
||
## Adapter identity
|
||
|
||
- Orpheus: {orpheus_manifest['adapter_huggingface_repo']}/tree/{orpheus_manifest['adapter_huggingface_revision']}
|
||
- Sesame: {sesame_manifest['adapter_huggingface_repo']}/tree/{sesame_manifest['adapter_huggingface_revision']}
|
||
- Exact revisions and every retained artifact hash are in `orpheus_manifest.json`, `sesame_manifest.json`, and `artifact_inventory.json`.
|
||
"""
|
||
(root / "REPORT.md").write_text(report, encoding="utf-8")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|