Files
L'électron rare f55093d6fe
ESP-IDF CI / Host Tests (Unity) (push) Successful in 1m8s
CI / firmware-native (push) Successful in 2m57s
Rust Protection Tests / Cargo test (host) (push) Failing after 3m21s
ESP-IDF CI / ESP-IDF Build (v5.4) (push) Failing after 6m55s
ESP-IDF CI / Memory Budget Gate (push) Has been skipped
qa-cicd-environments / qa-kxkm-s3-build (push) Successful in 8m53s
qa-cicd-environments / qa-sim-host (push) Successful in 2m2s
qa-cicd-environments / qa-kxkm-s3-memory-budget (push) Successful in 11m17s
chore: import KXKM Batterie Parallelator
Context: the project archive (KXKM_Batterie_Parallelator-main) had
no git history locally; a fresh repository is needed to host it on
git.saillant.cc (electron/KXKM_Batterie_Parallelator).

Approach: initialize a new repo on branch main, stage the archive
content, and harden .gitignore before the first commit.

Changes:
- Import the full project tree: firmware/, firmware-idf/,
  firmware-rs/, iosApp/, kxkm-bmu-app/, kxkm-api/, hardware/,
  docs/, specs/, scripts/, models/, tests/
- Keep project dotfiles tracked despite the trailing '.*' ignore
  rule: .github/, .claude/, .superpowers/, .gitattributes,
  .markdownlint.json
- Extend .gitignore: firmware/src/credentials.h (local secrets,
  template kept), kxkm-bmu-app/**/build/ (66 MB compiled iOS
  framework), .remember/ (session data)

Impact: the project can now be maintained on the self-hosted Gitea
forge with a clean, secret-free initial history.
2026-07-04 12:32:28 +02:00

192 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""
generate_dataset.py — Generate synthetic (battery context JSON -> French diagnostic) pairs
using Claude API for Qwen2.5-7B fine-tuning.
Usage:
python scripts/llm/generate_dataset.py \
--output data/llm/diagnostic_dataset_raw.jsonl \
--num-examples 1500 \
--scenario-mix balanced \
--api-key $ANTHROPIC_API_KEY
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import random
import sys
import time
from pathlib import Path
import anthropic
from tqdm import tqdm
from scenario_templates import (
SCENARIO_GENERATORS,
BatteryContext,
generate_context_for_scenario,
)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
datefmt="%H:%M:%S",
)
log = logging.getLogger("gen_dataset")
# ---------------------------------------------------------------------------
# Claude API generation
# ---------------------------------------------------------------------------
SYSTEM_PROMPT = (
"Tu es un expert en batteries LiFePO4/Li-ion pour systèmes de spectacle vivant "
"(KXKM BMU). Tu rédiges des diagnostics techniques concis en français."
)
USER_PROMPT_TEMPLATE = """\
Voici les données d'une batterie du parc KXKM:
{context_json}
Rédige un diagnostic concis en français (3-5 phrases):
1. État actuel de la batterie (santé, résistance interne, tension)
2. Tendance observée (évolution sur les derniers jours)
3. Recommandation d'action si nécessaire
Sois précis et technique. Utilise les unités (mΩ, mV, A, jours).
Termine par une classification de sévérité: [INFO], [WARNING] ou [CRITICAL].
"""
def generate_single_example(
client: anthropic.Anthropic,
context: BatteryContext,
model: str = "claude-sonnet-4-20250514",
max_retries: int = 3,
) -> dict | None:
"""Generate one (context, diagnostic) pair via Claude API."""
context_json = json.dumps(context.to_dict(), indent=2, ensure_ascii=False)
user_msg = USER_PROMPT_TEMPLATE.format(context_json=context_json)
for attempt in range(max_retries):
try:
response = client.messages.create(
model=model,
max_tokens=512,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": user_msg}],
temperature=0.7,
)
text = response.content[0].text.strip()
# Extract severity from [INFO]/[WARNING]/[CRITICAL] tag
severity = "info"
for tag in ["CRITICAL", "WARNING", "INFO"]:
if f"[{tag}]" in text:
severity = tag.lower()
break
return {
"context": context.to_dict(),
"scenario": context.scenario,
"diagnostic": text,
"severity": severity,
"model": model,
}
except anthropic.RateLimitError:
wait = 2 ** (attempt + 1)
log.warning("Rate limited, waiting %ds...", wait)
time.sleep(wait)
except anthropic.APIError as e:
log.error("API error: %s", e)
if attempt == max_retries - 1:
return None
time.sleep(1)
return None
def generate_dataset(
output_path: Path,
num_examples: int,
scenario_mix: str,
api_key: str,
model: str,
) -> None:
"""Generate full dataset and write to JSONL."""
client = anthropic.Anthropic(api_key=api_key)
output_path.parent.mkdir(parents=True, exist_ok=True)
# Distribute examples across scenarios
scenarios = list(SCENARIO_GENERATORS.keys())
if scenario_mix == "balanced":
per_scenario = num_examples // len(scenarios)
distribution = {s: per_scenario for s in scenarios}
# Distribute remainder
remainder = num_examples - per_scenario * len(scenarios)
for i, s in enumerate(scenarios[:remainder]):
distribution[s] += 1
else:
# Weighted: more degradation/anomaly examples
weights = {
"healthy": 0.15,
"early_degradation": 0.15,
"accelerated_degradation": 0.15,
"connection_issue": 0.12,
"fleet_outlier": 0.12,
"end_of_life": 0.10,
"post_replacement": 0.10,
"fleet_imbalance": 0.11,
}
distribution = {s: max(1, int(num_examples * w)) for s, w in weights.items()}
log.info("Distribution: %s (total: %d)", distribution, sum(distribution.values()))
written = 0
errors = 0
with open(output_path, "w", encoding="utf-8") as f:
for scenario, count in distribution.items():
log.info("Generating %d examples for scenario: %s", count, scenario)
for i in tqdm(range(count), desc=scenario):
context = generate_context_for_scenario(scenario)
example = generate_single_example(client, context, model=model)
if example:
f.write(json.dumps(example, ensure_ascii=False) + "\n")
written += 1
else:
errors += 1
# Rate limiting: ~30 req/min for Sonnet
time.sleep(2.0)
log.info("Done: %d written, %d errors -> %s", written, errors, output_path)
def main() -> None:
parser = argparse.ArgumentParser(description="Generate LLM diagnostic dataset")
parser.add_argument("--output", type=Path, default=Path("data/llm/diagnostic_dataset_raw.jsonl"))
parser.add_argument("--num-examples", type=int, default=1500)
parser.add_argument("--scenario-mix", choices=["balanced", "weighted"], default="balanced")
parser.add_argument("--api-key", default=os.environ.get("ANTHROPIC_API_KEY"))
parser.add_argument("--model", default="claude-sonnet-4-20250514")
args = parser.parse_args()
if not args.api_key:
log.error("ANTHROPIC_API_KEY required (--api-key or env var)")
sys.exit(1)
generate_dataset(
output_path=args.output,
num_examples=args.num_examples,
scenario_mix=args.scenario_mix,
api_key=args.api_key,
model=args.model,
)
if __name__ == "__main__":
main()