#!/usr/bin/env bash # Dispatches the HF Space container to one of the supported entrypoints # based on the ENTRYPOINT_MODE environment variable. # # Supported values: # serve (default) — training/serve.py (inference endpoint) # serve_adversarial Path D — adversarial-paraphrasing inference # endpoint (training/adversarial_serve.py). # Same /generate contract as `serve`, but # internally uses per-token adversarial decoding # guided by a RoBERTa-Large detector. Requires # ~16GB GPU (paraphraser 4-bit ~2.4GB + # detector fp16 ~1GB + KV-cache headroom on # t4-medium). See R36/R37/R38 in # .kiro/specs/humanizer-v3-rebuild-and-dpo/. # serve_v4 v4 — SauerkrautLM-Nemo-12B inference. Same # /generate contract as `serve` (training/serve.py). # Target adapter selected via ADAPTER_REPO env var # (LevArtesa/sft-humanizer-de-v3-{sauerkraut,dpo,rl}-lora); # for Stage_0 baseline set LOAD_ADAPTER=0 to use the # bare 12B model. See R29/R32/R38.1 in # .kiro/specs/humanizer-v4-sauerkraut-12b/. # train_sft_v4 v4 SFT training on SauerkrautLM-Nemo-12B against # finetune-sft-v4.jsonl from # LevArtesa/sft-humanizer-dataset-v4 # (training/train_sft_v4.py). See R12. # train_sft_v51 v5.1 SFT training on SauerkrautLM-Nemo-12B with # a manual prefix-masked-loss collator. Reuses # finetune-sft-v5.jsonl from # LevArtesa/sft-humanizer-dataset-v5 # (training/train_sft_v51.py). See # sft-v5-addendum.md §3 step 1.1 Option 2. # collect_dpo_pairs_v4 # v4 DPO preference pair collector — 4 completions # per block at temperatures {0.5, 0.7, 0.9, 1.1} # via the SFT v4 adapter, scored via GPTZero, plus # concatenation with the synthetic pool from R37 # into v4-dpo-combined.jsonl # (scripts/collect_dpo_pairs_v4.py). See R18-R20, # R37.8. # train_dpo_v4 v4 DPO training on top of SFT v4 adapter — # trl.DPOTrainer with β=0.1, lr=5e-6, 2 epochs # against v4-dpo-combined.jsonl # (training/train_dpo_v4.py). See R21. # train_rl_v4 v4 RL training pipeline — resumable GRPO on top # of SFT v4 (or DPO v4) adapter, gated on # Score_Cache (score-cache-v4.db) to avoid # re-paying GPTZero for identical completions # (training/train_rl_v4.py). See R25. # train v1 training (legacy, training/train.py) # train_v2 v2 training (training/train_v2.py) # train_sft SFT training pipeline (training/train_sft.py) # train_sft_v2 SFT training pipeline against the v2 dataset # (training/train_sft.py, env-overridden DATASET_PATH # + SFT_REPO_ID for the v2 LoRA adapter) # collect_dpo_pairs DPO preference pair collector — for each block # in the SFT v2 train-set, generates 4 completions # via the SFT v2 adapter at temperatures # {0.5, 0.7, 0.9, 1.1}, scores them via GPTZero, # and emits (prompt, chosen, rejected) triples # (scripts/collect_dpo_pairs.py) # train_dpo DPO training pipeline on top of SFT v2 adapter — # trains trl.DPOTrainer with β=0.1, lr=5e-6, # 2 epochs against the preference pairs emitted # by collect_dpo_pairs (training/train_dpo.py) # train_rl_v3 RL v3 training pipeline with safeguards — # resumable GRPO on top of the SFT v2 (or DPO) # adapter, gated on Score_Cache to avoid # re-paying GPTZero for identical completions # (training/train_rl_v3.py) # filter_dataset_v2 Binoculars filter for v2 dataset # (scripts/filter_with_binoculars.py) # validate_dataset GPTZero validation pass over an existing dataset # (scripts/validate_dataset_with_gptzero.py) # rebuild_dataset Resumable rebuild driver around the GPTZero # validator (scripts/rebuild_dataset_with_gptzero.py) # rebuild_dataset_aggressive # Aggressive-prompt variant of rebuild_dataset — # swaps the production AI_Version_Generator prompt # for _AGGRESSIVE_SYSTEM_PROMPT # (scripts/rebuild_dataset_with_gptzero.py # --aggressive-prompt) # evaluate v1 evaluation (legacy, training/evaluate.py) # evaluate_sft SFT-adapter evaluation harness # (training/evaluate_sft.py) # # Any extra arguments passed to this script are forwarded to the chosen # Python command (this is required for filter_dataset_v2, which takes # CLI flags such as --raw-in / --final-out / --threshold). # # For testability, passing --dry-run as the first argument prints the # command that WOULD be executed and exits with status 0 without running # anything. set -euo pipefail # Ensure standalone Python scripts (invoked by absolute path, not -m) can # still import from the top-level `training/` package. export PYTHONPATH="/app:${PYTHONPATH:-}" MODE="${ENTRYPOINT_MODE:-serve}" # build the command for the selected mode case "$MODE" in serve) CMD=("python" "-m" "training.serve") ;; serve_adversarial) # Path D — adversarial paraphrasing inference endpoint. # Mirrors the `serve` contract (POST /generate with the same # payload schema) but internally uses a per-token adversarial # decoding loop where each candidate token is scored through a # RoBERTa-Large detector and the lowest-AI-probability token is # selected. See ``training/adversarial_serve.py`` and R36/R37/R38 # in ``.kiro/specs/humanizer-v3-rebuild-and-dpo/``. # # The default detector ``roberta-large-openai-detector`` (~1.4GB # fp32 / <1GB fp16) is downloaded from HF on first start and # cached in the Space's shared HF cache. ``HF_TOKEN`` only matters # if the (optional) ``ADAPTER_REPO`` is private — the detector # itself is public. CMD=("python" "-m" "training.adversarial_serve") ;; serve_v4) # v4 — SauerkrautLM-Nemo-12B inference. Same /generate contract as # `serve` mode (training/serve.py). The target adapter is selected # via ADAPTER_REPO env var (one of: # LevArtesa/sft-humanizer-de-v3-sauerkraut-lora, # LevArtesa/sft-humanizer-de-v3-dpo-lora, # LevArtesa/sft-humanizer-de-v3-rl-lora). # For Stage_0 baseline (R38), set LOAD_ADAPTER=0 to use the bare # SauerkrautLM-Nemo-12B without any LoRA. See R29 / R38.1 / # R32 in .kiro/specs/humanizer-v4-sauerkraut-12b/. CMD=("python" "-m" "training.serve") ;; train_sft_v4) # v4 SFT training on SauerkrautLM-Nemo-12B against # finetune-sft-v4.jsonl. Pre-fetch dataset from # LevArtesa/sft-humanizer-dataset-v4 (new dataset repo created in # Stage_1). HF_TOKEN required since dataset repo is private. See # R12 in .kiro/specs/humanizer-v4-sauerkraut-12b/. mkdir -p /app/dataset if [ ! -f /app/dataset/finetune-sft-v4.jsonl ]; then echo "Downloading finetune-sft-v4.jsonl from LevArtesa/sft-humanizer-dataset-v4 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v4 \ finetune-sft-v4.jsonl fi # HF Spaces orchestrator probes ``$PORT`` (7860) and kills the # workload at the 30-minute "not healthy" timeout if no listener # ever appears. SFT v4 training is a multi-hour batch GPU job # that never opens any port, so we spawn a tiny background HTTP # healthcheck server FIRST and trap-kill it on case exit. Mirrors # the existing ``prep_dataset_v4`` workaround. HF_PORT="${PORT:-7860}" python3 -m http.server "$HF_PORT" --bind 0.0.0.0 >/tmp/healthcheck.log 2>&1 & HEALTHCHECK_PID=$! trap 'kill $HEALTHCHECK_PID 2>/dev/null || true' EXIT echo "Health-check HTTP server started on port $HF_PORT (pid=$HEALTHCHECK_PID)" # --yes skips the interactive Cost_Estimator prompt — required in # non-interactive Docker (no stdin attached). CMD=("python" "-m" "training.train_sft_v4" "--yes") ;; train_sft_v5) # v5 SFT training on SauerkrautLM-Nemo-12B against # finetune-sft-v5.jsonl (~699 records: 500 fresh academic German # pairs + 199 v3 holdout). Pre-fetch dataset from # LevArtesa/sft-humanizer-dataset-v5 (new dataset repo created # during Etap 1). HF_TOKEN required since dataset repo is private. # See sft-v5-addendum.md §3 Etap 1 step 1.4 in # .kiro/specs/humanizer-v4-sauerkraut-12b/. mkdir -p /app/dataset if [ ! -f /app/dataset/finetune-sft-v5.jsonl ]; then echo "Downloading finetune-sft-v5.jsonl from LevArtesa/sft-humanizer-dataset-v5 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v5 \ finetune-sft-v5.jsonl fi # HF Spaces orchestrator probes ``$PORT`` (7860) and kills the # workload at the 30-minute "not healthy" timeout if no listener # ever appears. SFT v5 training is a multi-hour batch GPU job # that never opens any port, so we spawn a tiny background HTTP # healthcheck server FIRST and trap-kill it on case exit. Mirrors # the existing ``train_sft_v4`` workaround. HF_PORT="${PORT:-7860}" python3 -m http.server "$HF_PORT" --bind 0.0.0.0 >/tmp/healthcheck.log 2>&1 & HEALTHCHECK_PID=$! trap 'kill $HEALTHCHECK_PID 2>/dev/null || true' EXIT echo "Health-check HTTP server started on port $HF_PORT (pid=$HEALTHCHECK_PID)" # --yes skips the interactive Cost_Estimator prompt — required in # non-interactive Docker (no stdin attached). CMD=("python" "-m" "training.train_sft_v5" "--yes") ;; train_sft_v51) # v5.1 SFT training on SauerkrautLM-Nemo-12B against # finetune-sft-v5.jsonl (REUSED from v5 — same ~699 records: 500 # fresh academic German pairs + 199 v3 holdout). The trainer adds # a manual prefix-masked-loss collator (Option 2 in # sft-v5-addendum.md §3 step 1.1) that finishes the Bug C work # TRL's ``assistant_only_loss=True`` could not do automatically # on the Mistral chat template. Pre-fetch dataset from # LevArtesa/sft-humanizer-dataset-v5 (same repo as v5; nothing # new on Hub). HF_TOKEN required since dataset repo is private. # See sft-v5-addendum.md §3 Etap 1 step 1.1 in # .kiro/specs/humanizer-v4-sauerkraut-12b/. mkdir -p /app/dataset if [ ! -f /app/dataset/finetune-sft-v5.jsonl ]; then echo "Downloading finetune-sft-v5.jsonl from LevArtesa/sft-humanizer-dataset-v5 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v5 \ finetune-sft-v5.jsonl fi # HF Spaces orchestrator probes ``$PORT`` (7860) and kills the # workload at the 30-minute "not healthy" timeout if no listener # ever appears. SFT v5.1 training is a multi-hour batch GPU job # that never opens any port, so we spawn a tiny background HTTP # healthcheck server FIRST and trap-kill it on case exit. Mirrors # the existing ``train_sft_v5`` workaround. HF_PORT="${PORT:-7860}" python3 -m http.server "$HF_PORT" --bind 0.0.0.0 >/tmp/healthcheck.log 2>&1 & HEALTHCHECK_PID=$! trap 'kill $HEALTHCHECK_PID 2>/dev/null || true' EXIT echo "Health-check HTTP server started on port $HF_PORT (pid=$HEALTHCHECK_PID)" # --yes skips the interactive Cost_Estimator prompt — required in # non-interactive Docker (no stdin attached). CMD=("python" "-m" "training.train_sft_v51" "--yes") ;; collect_dpo_pairs_v4) # v4 DPO preference pair collector — generates 4 completions per # block in the SFT v4 train-set at temperatures {0.5, 0.7, 0.9, 1.1} # via the SFT v4 adapter, scores them via GPTZero, and emits # (prompt, chosen, rejected) triples to v4-dpo-self.jsonl. Plus # concatenates with the synthetic pool from R37 into # v4-dpo-combined.jsonl. Pre-fetches finetune-sft-v4.jsonl, # v4-dpo-synthetic.jsonl, and any prior checkpoint. HF_TOKEN # required. See R18-R20, R37.8 in # .kiro/specs/humanizer-v4-sauerkraut-12b/. mkdir -p /app/dataset if [ ! -f /app/dataset/finetune-sft-v4.jsonl ]; then echo "Downloading finetune-sft-v4.jsonl from LevArtesa/sft-humanizer-dataset-v4 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v4 \ finetune-sft-v4.jsonl fi if [ ! -f /app/dataset/v4-dpo-synthetic.jsonl ]; then echo "Downloading v4-dpo-synthetic.jsonl from LevArtesa/sft-humanizer-dataset-v4 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v4 \ v4-dpo-synthetic.jsonl fi huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v4 \ v4-dpo-pairs-checkpoint.jsonl 2>/dev/null \ || echo "(no prior v4 DPO pairs checkpoint)" CMD=("python" "/app/scripts/collect_dpo_pairs_v4.py") ;; train_dpo_v4) # v4 DPO training on top of SFT v4 adapter — trl.DPOTrainer with # β=0.1, lr=5e-6, 2 epochs against v4-dpo-combined.jsonl. See R21 # in .kiro/specs/humanizer-v4-sauerkraut-12b/. mkdir -p /app/dataset if [ ! -f /app/dataset/v4-dpo-combined.jsonl ]; then echo "Downloading v4-dpo-combined.jsonl from LevArtesa/sft-humanizer-dataset-v4 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v4 \ v4-dpo-combined.jsonl fi CMD=("python" "-m" "training.train_dpo_v4") ;; train_rl_v4) # v4 RL training pipeline. GRPO with safeguards: resumable on top # of SFT v4 (or DPO v4) adapter, gated on Score_Cache to avoid # re-paying GPTZero for identical completions. Pre-fetches # finetune-sft-v4.jsonl + any prior score-cache-v4.db. See R25 # in .kiro/specs/humanizer-v4-sauerkraut-12b/. mkdir -p /app/dataset if [ ! -f /app/dataset/finetune-sft-v4.jsonl ]; then echo "Downloading finetune-sft-v4.jsonl from LevArtesa/sft-humanizer-dataset-v4 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v4 \ finetune-sft-v4.jsonl fi huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v4 \ score-cache-v4.db 2>/dev/null \ || echo "(no prior score cache v4)" CMD=("python" "-m" "training.train_rl_v4") ;; prep_backtrans_only) # v4 Stage_1 — filter_backtrans only mode (R8). Used to isolate the # ~3-4h filter step from the costly synth step. Runs Pass 1/2/3, # uploads the JSONL + meta sidecar to the dataset repo, then # pauses the Space. The operator triggers fachsprache_anchor and # synthesize_dpo_pairs separately by switching ENTRYPOINT_MODE. # # NOTE: HF Spaces orchestrator probes port 7860 and kills the # workload at the 30-minute "not healthy" timeout if no listener # ever appears. The filter is a 3-4h batch job that never opens # any port, so we spawn a tiny background HTTP healthcheck server # FIRST and let it run for the lifetime of the container. The # filter then runs in the foreground; on completion bash exits # and the container terminates with the healthcheck still bound. mkdir -p /app/dataset set -e echo '=== Spawning healthcheck server on port 7860 (HF Space requirement) ===' python -c "import http.server, socketserver, threading; h = http.server.SimpleHTTPRequestHandler; srv = socketserver.TCPServer(('0.0.0.0', 7860), h); threading.Thread(target=srv.serve_forever, daemon=True).start(); import time; time.sleep(86400)" & HEALTHCHECK_PID=$! sleep 2 echo " healthcheck pid=$HEALTHCHECK_PID" echo '=== Stage_1 (backtrans only): filter_backtrans ===' python /app/scripts/filter_backtrans.py \ --output /app/dataset/v4-backtrans-filtered.jsonl \ --meta /app/dataset/v4-backtrans-meta.json \ --target-size 50000 \ --seed 42 echo '=== filter_backtrans done — uploading to dataset repo ===' # Create the dataset repo on first upload (idempotent). python -c "import os; from huggingface_hub import HfApi; HfApi(token=os.environ['HF_TOKEN']).create_repo(repo_id='LevArtesa/sft-humanizer-dataset-v4', repo_type='dataset', exist_ok=True, private=False)" huggingface-cli upload \ --repo-type dataset \ LevArtesa/sft-humanizer-dataset-v4 \ /app/dataset/v4-backtrans-filtered.jsonl \ v4-backtrans-filtered.jsonl huggingface-cli upload \ --repo-type dataset \ LevArtesa/sft-humanizer-dataset-v4 \ /app/dataset/v4-backtrans-meta.json \ v4-backtrans-meta.json echo '=== upload complete. Pausing Space. ===' kill "$HEALTHCHECK_PID" 2>/dev/null || true python -c "import os; from huggingface_hub import HfApi; HfApi(token=os.environ['HF_TOKEN']).pause_space(os.environ.get('SPACE_ID', 'LevArtesa/humanizer-v4-sauerkraut'))" || true CMD=("echo" "Stage_1 backtrans-only complete — Space paused") ;; prep_dataset_v4) # v4 Stage_1 dataset preparation pipeline. Runs all four steps in # sequence on the Space: # 1. filter_backtrans.py -> dataset/v4-backtrans-filtered.jsonl (50k) # 2. build_fachsprache_anchor.py -> dataset/v4-fachsprache-anchor.jsonl (~12k) # 3. synthesize_dpo_pairs.py -> dataset/v4-dpo-synthetic.jsonl (~7500, OpenRouter) # 4. build_sft_v4_dataset.py -> dataset/finetune-sft-v4.jsonl (atomic concat) # Plus uploads results to LevArtesa/sft-humanizer-dataset-v4 dataset # repo. CPU-bound — t4-medium GPU is unused but Space hardware was # already requested by the operator. OPENROUTER_API_KEY must be set # in the Space secrets. HF_TOKEN required for cloud_sync. See R8, # R9, R10, R11, R37 in # .kiro/specs/humanizer-v4-sauerkraut-12b/. # # NOTE (2026-XX-XX): HF Spaces orchestrator probes ``$PORT`` (7860) # and kills the workload at the 30-minute "not healthy" timeout if # nothing is bound. The Stage_1 pipeline is a multi-hour batch job # that never opens any port, so we spawn a tiny background HTTP # healthcheck server FIRST and trap-kill it on case exit. Mirrors # the existing ``prep_backtrans_only`` workaround. mkdir -p /app/dataset set -e HF_PORT="${PORT:-7860}" python3 -m http.server "$HF_PORT" --bind 0.0.0.0 >/tmp/healthcheck.log 2>&1 & HEALTHCHECK_PID=$! trap 'kill $HEALTHCHECK_PID 2>/dev/null || true' EXIT echo "Health-check HTTP server started on port $HF_PORT (pid=$HEALTHCHECK_PID)" echo '=== Stage_1 step 1/4: filter_backtrans ===' python /app/scripts/filter_backtrans.py \ --output /app/dataset/v4-backtrans-filtered.jsonl \ --target-size 50000 \ --seed 42 echo '=== Stage_1 step 2/4: build_fachsprache_anchor ===' python /app/scripts/build_fachsprache_anchor.py \ --output /app/dataset/v4-fachsprache-anchor.jsonl \ --target-size 12000 echo '=== Stage_1 step 3/4: synthesize_dpo_pairs ===' python /app/scripts/synthesize_dpo_pairs.py \ --output /app/dataset/v4-dpo-synthetic.jsonl \ --target 7500 \ --cost-cap-usd 350 echo '=== Stage_1 step 4/4: build_sft_v4_dataset ===' python /app/scripts/build_sft_v4_dataset.py \ --backtrans /app/dataset/v4-backtrans-filtered.jsonl \ --fachsprache /app/dataset/v4-fachsprache-anchor.jsonl \ --v3-holdout /app/dataset/finetune-sft-v2.jsonl \ --output /app/dataset/finetune-sft-v4.jsonl \ --meta /app/dataset/finetune-sft-v4.meta.json \ --sanity-sample /app/dataset/finetune-sft-v4.sanity-sample.jsonl echo '=== Stage_1 done. Uploading to dataset repo ===' # Upload all v4 dataset artifacts via huggingface-cli. The dataset # repo is created on first upload via api.create_repo. huggingface-cli upload \ --repo-type dataset \ LevArtesa/sft-humanizer-dataset-v4 \ /app/dataset/finetune-sft-v4.jsonl \ finetune-sft-v4.jsonl huggingface-cli upload \ --repo-type dataset \ LevArtesa/sft-humanizer-dataset-v4 \ /app/dataset/finetune-sft-v4.meta.json \ finetune-sft-v4.meta.json huggingface-cli upload \ --repo-type dataset \ LevArtesa/sft-humanizer-dataset-v4 \ /app/dataset/v4-backtrans-filtered.jsonl \ v4-backtrans-filtered.jsonl huggingface-cli upload \ --repo-type dataset \ LevArtesa/sft-humanizer-dataset-v4 \ /app/dataset/v4-fachsprache-anchor.jsonl \ v4-fachsprache-anchor.jsonl huggingface-cli upload \ --repo-type dataset \ LevArtesa/sft-humanizer-dataset-v4 \ /app/dataset/v4-dpo-synthetic.jsonl \ v4-dpo-synthetic.jsonl huggingface-cli upload \ --repo-type dataset \ LevArtesa/sft-humanizer-dataset-v4 \ /app/dataset/finetune-sft-v4.sanity-sample.jsonl \ finetune-sft-v4.sanity-sample.jsonl echo '=== Stage_1 uploads complete. Pausing Space. ===' # Auto-pause via shutdown endpoint (best-effort, exit 0 on failure). python -c "import os; from huggingface_hub import HfApi; HfApi(token=os.environ['HF_TOKEN']).pause_space(os.environ.get('SPACE_ID', 'LevArtesa/humanizer-v4-sauerkraut'))" || true CMD=("echo" "Stage_1 complete — Space paused") ;; train_german_guide_v5) # v5 Stage_V_0 — fine-tune ``xlm-roberta-base`` as a German guide # detector for Path D adversarial decoding (R19). Binary # classifier trained on the 530 etap1 academic + 199 v3 holdout # records from ``finetune-sft-v5.jsonl`` (``human`` → label=0, # ``ai`` → label=1, R19.1). On completion the classifier is # published to ``LevArtesa/german-ai-detector-v5-guide`` (R19.3). # Pre-fetches the v5 dataset from # ``LevArtesa/sft-humanizer-dataset-v5``; HF_TOKEN required since # the dataset repo is private. ``.env`` (if present at /app/.env) # is sourced first per design D4 so HF_TOKEN / GPTZERO_API_KEY are # available on dev machines without manual export. Hardware tier # ``t4-medium`` (R37.4). See # .kiro/specs/humanizer-v5-detector-in-the-loop/ R19. set -a; [ -f /app/.env ] && source /app/.env; set +a mkdir -p /app/dataset if [ ! -f /app/dataset/finetune-sft-v5.jsonl ]; then echo "Downloading finetune-sft-v5.jsonl from LevArtesa/sft-humanizer-dataset-v5 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v5 \ finetune-sft-v5.jsonl fi # HF Spaces orchestrator probes ``$PORT`` (7860) and kills the # workload at the 30-minute "not healthy" timeout if no listener # ever appears. Detector training is a multi-minute batch GPU # job that never opens any port, so we spawn a tiny background # HTTP healthcheck server FIRST and trap-kill it on case exit. # Mirrors the existing ``train_sft_v5`` workaround. HF_PORT="${PORT:-7860}" python3 -m http.server "$HF_PORT" --bind 0.0.0.0 >/tmp/healthcheck.log 2>&1 & HEALTHCHECK_PID=$! trap 'kill $HEALTHCHECK_PID 2>/dev/null || true' EXIT echo "Health-check HTTP server started on port $HF_PORT (pid=$HEALTHCHECK_PID)" CMD=("python" "-m" "training.train_german_guide_v5") ;; serve_path_d_v5) # v5 Stage_V_0 — Path D adversarial-paraphrasing inference with # the German guide detector replacing the v3 RoBERTa-OpenAI # default. Reuses ``training/adversarial_serve.py`` unchanged # except for the additive ``GUIDE_DETECTOR_REPO`` env-var gate # (R11.5 / R20.1). Loads SauerkrautLM-Nemo-12B with the v5.1 # LoRA adapter ``LevArtesa/sft-humanizer-de-v3-sauerkraut-v51-lora`` # — no new policy training in Stage_V_0 (R20.2). Hardware tier # ``t4-medium`` (R20.4 / R37.4). See # .kiro/specs/humanizer-v5-detector-in-the-loop/ R20. set -a; [ -f /app/.env ] && source /app/.env; set +a export GUIDE_DETECTOR_REPO="${GUIDE_DETECTOR_REPO:-LevArtesa/german-ai-detector-v5-guide}" export ADAPTER_REPO="${ADAPTER_REPO:-LevArtesa/sft-humanizer-de-v3-sauerkraut-v51-lora}" # MODEL_REPO MUST be the 12B base the v5.1 LoRA was trained on # (design §3.1 / R20.2). adversarial_serve.py defaults MODEL_REPO to # Qwen/Qwen3-4B (the v3 Path D paraphraser); leaving it unset here would # make PeftModel.from_pretrained fail to apply the 12B LoRA onto the 4B # base, get swallowed by the adapter-load except, and SILENTLY serve bare # Qwen3-4B — burning the whole serve GPU run + 27 GPTZero blocks on a # meaningless result. Pin it to SauerkrautLM-Nemo-12B (operator override # still wins via ${MODEL_REPO:-...}). export MODEL_REPO="${MODEL_REPO:-VAGOsolutions/SauerkrautLM-Nemo-12b-Instruct}" CMD=("python" "-m" "training.adversarial_serve") ;; train_surrogate_v5) # v5 Stage_V_1 — three-phase orchestration of the GPTZero # surrogate detector (R22-R24). The phase is selected by the # ``V5_SURROGATE_PHASE`` env var: # dataset — assemble + GPTZero-score 5000-10000 German # paragraphs into ``dataset/v5-surrogate-train.jsonl`` # (R22, atomic per-record write per R4) # train — fine-tune ``xlm-roberta-base`` regressor on the # assembled dataset and publish to # ``LevArtesa/gptzero-surrogate-de-v5`` (R23) # validate — compute R² on the held-out 20% and write # ``decision-gate-V-1.json`` (R24) # Best-effort pre-fetches the partial # ``v5-surrogate-train.jsonl`` from the HF dataset repo so a # resumed run skips already-scored paragraphs via score_cache # (R15.3, R22.3). HF_TOKEN / GPTZERO_API_KEY / OPENROUTER_API_KEY # required — sourced from ``/app/.env`` if present per design D4. # Hardware tier ``t4-medium`` (R37.4 — 270M xlm-roberta fits). set -a; [ -f /app/.env ] && source /app/.env; set +a mkdir -p /app/dataset huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v5 \ v5-surrogate-train.jsonl 2>/dev/null \ || echo "(no prior v5 surrogate dataset — fresh assembly)" # The held-out 20% validation split is REQUIRED by the train/validate # phases for the R² gate (R23.2 / R24); pre-fetch it best-effort alongside # the train split. Absent in the `dataset` phase (it is produced there), so # a miss is non-fatal. huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v5 \ v5-surrogate-validation.jsonl 2>/dev/null \ || echo "(no v5 surrogate validation split — fresh assembly will produce it)" HF_PORT="${PORT:-7860}" python3 -m http.server "$HF_PORT" --bind 0.0.0.0 >/tmp/healthcheck.log 2>&1 & HEALTHCHECK_PID=$! trap 'kill $HEALTHCHECK_PID 2>/dev/null || true' EXIT echo "Health-check HTTP server started on port $HF_PORT (pid=$HEALTHCHECK_PID)" CMD=("python" "-m" "training.train_surrogate_v5") ;; train_grpo_v5) # v5 Stage_V_2 — GRPO on SauerkrautLM-Nemo-12B + v5.1 LoRA with # the GPTZero surrogate as cheap reward and a KL anchor to the # frozen v5.1 reference policy (R26-R29). The trainable adapter # is published to ``LevArtesa/grpo-humanizer-de-v5-lora`` (NEW # repo, never overwrites v5.1, R26.5). Pre-fetches the v5.1 # source dataset and best-effort pre-fetches any prior GRPO # checkpoint dir + cumulative reward log so a Space restart # resumes from the last persisted GRPO step (R16.3). Hardware # tier ``a10g-large`` (R37.4) — 12B base + LoRA r=64 trainable + # LoRA reference + surrogate (270M) + sentence-transformer # together require ~22GB peak. See # .kiro/specs/humanizer-v5-detector-in-the-loop/ R26-R29. set -a; [ -f /app/.env ] && source /app/.env; set +a mkdir -p /app/dataset if [ ! -f /app/dataset/finetune-sft-v5.jsonl ]; then echo "Downloading finetune-sft-v5.jsonl from LevArtesa/sft-humanizer-dataset-v5 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v5 \ finetune-sft-v5.jsonl fi huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v5 \ v5-grpo-rewards.jsonl 2>/dev/null \ || echo "(no prior GRPO reward log — fresh run)" huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v5 \ v5-grpo-checkpoint.tar 2>/dev/null \ || echo "(no prior GRPO checkpoint — fresh run)" HF_PORT="${PORT:-7860}" python3 -m http.server "$HF_PORT" --bind 0.0.0.0 >/tmp/healthcheck.log 2>&1 & HEALTHCHECK_PID=$! trap 'kill $HEALTHCHECK_PID 2>/dev/null || true' EXIT echo "Health-check HTTP server started on port $HF_PORT (pid=$HEALTHCHECK_PID)" CMD=("python" "-m" "training.train_grpo_v5") ;; serve_grpo_v5) # v5 Stage_V_2 / Stage_V_4 — production serve mode for the # GRPO-trained adapter ``LevArtesa/grpo-humanizer-de-v5-lora``. # Mirrors the ``serve`` contract (training/serve.py) with # ``ADAPTER_REPO`` pinned to the v5 GRPO output (R34.2). Hardware # tier ``t4-medium``. set -a; [ -f /app/.env ] && source /app/.env; set +a export ADAPTER_REPO="${ADAPTER_REPO:-LevArtesa/grpo-humanizer-de-v5-lora}" CMD=("python" "-m" "training.serve") ;; train_authormist_v5) # v5 Stage_V_3 — fallback fine-tune of the published AuthorMist # Originality model (1-3B base) on the 530 etap1 academic German # pairs (R31). Triggered ONLY when Stage_V_2 yields # ``success_rate ∈ [0.4, 0.6)`` per R30.5. The base repo is # selected via the ``AUTHORMIST_BASE_REPO`` env var, defaulting # to ``Aman90101/test`` (R31.1). The trained adapter is published # to ``LevArtesa/authormist-humanizer-de-v5-lora`` (NEW repo). # Hardware tier ``t4-medium`` — 1-3B base with LoRA r=32 fits in # 16GB (R31.4 / R37.4). See # .kiro/specs/humanizer-v5-detector-in-the-loop/ R31. set -a; [ -f /app/.env ] && source /app/.env; set +a export AUTHORMIST_BASE_REPO="${AUTHORMIST_BASE_REPO:-Aman90101/test}" mkdir -p /app/dataset if [ ! -f /app/dataset/finetune-sft-v5.jsonl ]; then echo "Downloading finetune-sft-v5.jsonl from LevArtesa/sft-humanizer-dataset-v5 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v5 \ finetune-sft-v5.jsonl fi HF_PORT="${PORT:-7860}" python3 -m http.server "$HF_PORT" --bind 0.0.0.0 >/tmp/healthcheck.log 2>&1 & HEALTHCHECK_PID=$! trap 'kill $HEALTHCHECK_PID 2>/dev/null || true' EXIT echo "Health-check HTTP server started on port $HF_PORT (pid=$HEALTHCHECK_PID)" CMD=("python" "-m" "training.train_authormist_v5" "--yes") ;; serve_authormist_v5) # v5 Stage_V_3 / Stage_V_4 — production serve mode for the # AuthorMist fallback adapter # ``LevArtesa/authormist-humanizer-de-v5-lora``. Mirrors the # ``serve`` contract (training/serve.py) with both the adapter # repo and the AuthorMist base model selected via env vars (so # an operator can swap to a different AuthorMist checkpoint # without code change, R31.1 / R34.2). ``BASE_MODEL_REPO`` is # propagated from ``AUTHORMIST_BASE_REPO`` for parity with the # ``train_authormist_v5`` case branch. set -a; [ -f /app/.env ] && source /app/.env; set +a export AUTHORMIST_BASE_REPO="${AUTHORMIST_BASE_REPO:-Aman90101/test}" export ADAPTER_REPO="${ADAPTER_REPO:-LevArtesa/authormist-humanizer-de-v5-lora}" export BASE_MODEL_REPO="$AUTHORMIST_BASE_REPO" # serve.py reads the base via MODEL_REPO (os.environ.get("MODEL_REPO", ...)), # NOT BASE_MODEL_REPO. Without this the serve would silently load the # Qwen3-4B default instead of the AuthorMist base (Qwen2.5-3B), and our # 3B LoRA would fail to apply onto the 4B base — burning the serve GPU + # 27 GPTZero blocks on a meaningless result. Pin MODEL_REPO to the # AuthorMist base (operator override still wins via the env above). export MODEL_REPO="$AUTHORMIST_BASE_REPO" CMD=("python" "-m" "training.serve") ;; train) CMD=("python" "-m" "training.train") ;; train_v2) CMD=("python" "-m" "training.train_v2") ;; train_sft) # Pre-fetch the SFT dataset from the private HF dataset repo before # invoking the trainer. Skipped if the file is already on disk so a # Space restart resumes from local cache without a redundant # download. ``HF_TOKEN`` is required since the dataset repo is # private — the Space already has it as a secret. mkdir -p /app/dataset if [ ! -f /app/dataset/finetune-sft.jsonl ]; then echo "Downloading finetune-sft.jsonl from LevArtesa/sft-humanizer-dataset-v1 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v1 \ finetune-sft.jsonl fi CMD=("python" "-m" "training.train_sft") ;; train_sft_v2) # SFT training against the v2 (aggressively-rebuilt) dataset. # Re-uses ``training/train_sft.py`` unchanged (Requirement 6.16 # reuse mandate) — only the dataset path and destination LoRA # repo differ from ``train_sft``. Both are exported as env vars # so the trainer module picks them up without any code change. export DATASET_PATH=/app/dataset/finetune-sft-v2.jsonl export SFT_REPO_ID=LevArtesa/sft-humanizer-de-v2-lora mkdir -p /app/dataset if [ ! -f /app/dataset/finetune-sft-v2.jsonl ]; then echo "Downloading finetune-sft-v2.jsonl from LevArtesa/sft-humanizer-dataset-v2 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v2 \ finetune-sft-v2.jsonl fi if [ ! -f /app/dataset/holdout-v2.jsonl ]; then echo "Downloading holdout-v2.jsonl from LevArtesa/sft-humanizer-dataset-v2 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v2 \ holdout-v2.jsonl fi CMD=("python" "-m" "training.train_sft") ;; collect_dpo_pairs) # DPO preference pair collector — generates 4 completions per # block at temperatures {0.5, 0.7, 0.9, 1.1} via the SFT v2 # adapter, scores them via GPTZero, and emits # (prompt, chosen, rejected) triples to dpo-pairs.jsonl. # Pre-fetches the SFT v2 train-set so the collector can read its # prompts without a separate download, plus any prior DPO pair # checkpoint so a Space restart resumes from where it left off. # ``HF_TOKEN`` is required since both dataset repos are private. mkdir -p /app/dataset if [ ! -f /app/dataset/finetune-sft-v2.jsonl ]; then echo "Downloading finetune-sft-v2.jsonl from LevArtesa/sft-humanizer-dataset-v2 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v2 \ finetune-sft-v2.jsonl fi # Best-effort pre-fetch of any prior DPO pair checkpoint so a # restarted container resumes from the last persisted block. If # no checkpoint exists yet on the dataset repo, the collector # simply starts fresh — the missing-file error is swallowed. huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v2 \ dpo-pairs-checkpoint.jsonl 2>/dev/null \ || echo "(no prior DPO checkpoint)" CMD=("python" "/app/scripts/collect_dpo_pairs.py") ;; train_dpo) # DPO training on top of the SFT v2 adapter — consumes the # preference pairs emitted by ``collect_dpo_pairs`` and trains # ``trl.DPOTrainer`` with β=0.1, learning_rate=5e-6, # num_train_epochs=2 (per Requirement 18). Pre-fetches the # ``dpo-pairs.jsonl`` file from the v2 dataset repo unless it is # already on disk, so a Space restart resumes from local cache # without a redundant download. ``HF_TOKEN`` is required since # the dataset repo is private — the Space already has it as a # secret. mkdir -p /app/dataset if [ ! -f /app/dataset/dpo-pairs.jsonl ]; then echo "Downloading dpo-pairs.jsonl from LevArtesa/sft-humanizer-dataset-v2 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v2 \ dpo-pairs.jsonl fi CMD=("python" "-m" "training.train_dpo") ;; train_rl_v3) # RL v3 training pipeline with safeguards. Resumable GRPO on top # of the SFT v2 (or DPO) adapter that gates every reward lookup # on a persistent Score_Cache to avoid re-paying GPTZero for # identical completions across restarts. Pre-fetches the v2 SFT # dataset and hold-out (used as prompt source for rollouts) plus # any prior score-cache so the cache survives Space restarts. # ``HF_TOKEN`` is required since the dataset repo is private. mkdir -p /app/dataset if [ ! -f /app/dataset/finetune-sft-v2.jsonl ]; then echo "Downloading finetune-sft-v2.jsonl from LevArtesa/sft-humanizer-dataset-v2 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v2 \ finetune-sft-v2.jsonl fi if [ ! -f /app/dataset/holdout-v2.jsonl ]; then echo "Downloading holdout-v2.jsonl from LevArtesa/sft-humanizer-dataset-v2 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v2 \ holdout-v2.jsonl fi # Best-effort pre-fetch of any prior score cache so the RL run # resumes with all previously-paid GPTZero scores intact (saves # real $$ across Space restarts). If no cache exists yet on the # dataset repo, the trainer simply starts with an empty cache — # the missing-file error is swallowed. huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v2 \ score-cache.db 2>/dev/null \ || echo "(no prior score cache)" CMD=("python" "-m" "training.train_rl_v3") ;; filter_dataset_v2) # Pre-fetch the raw JSONL from the private HF dataset repo. Using # huggingface-cli avoids an extra Python wrapper and respects # HF_TOKEN / HF_HOME automatically. mkdir -p /app/dataset if [ ! -f /app/dataset/v2-raw.jsonl ]; then echo "Downloading v2-raw.jsonl from LevArtesa/grpo-humanizer-dataset-v2 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/grpo-humanizer-dataset-v2 \ v2-raw.jsonl fi # Reduce CUDA fragmentation — attention allocations for the # Falcon-7B pair at len=2048 are large and transient. export PYTORCH_CUDA_ALLOC_CONF="expandable_segments:True" CMD=( "python" "/app/scripts/filter_with_binoculars.py" "--raw-in" "/app/dataset/v2-raw.jsonl" "--final-out" "/app/dataset/finetune-v2.jsonl" "--threshold" "0.7" "--batch-size" "8" ) ;; evaluate) CMD=("python" "-m" "training.evaluate") ;; evaluate_sft) # Pre-fetch the hold-out JSONL from the SFT dataset repo so the # evaluator can score it without re-running SFTPreparer (same # seed=42 → identical 40 records). The progress JSONL written # alongside ``--report-path`` makes the run resumable across # container restarts: rerunning this command picks up after the # last persisted record. Pre-fetch any prior progress JSONL so # the resume actually works after a Space restart. mkdir -p /app/dataset if [ ! -f /app/dataset/holdout.jsonl ]; then echo "Downloading holdout.jsonl from LevArtesa/sft-humanizer-dataset-v1 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v1 \ holdout.jsonl fi mkdir -p /home/user/output # Try to fetch a previously persisted progress file so a restarted # container resumes from where it left off (the previous run on # 2026-05-15 died at record ~32/40 due to KV-cache blow-up at # ``max_new_tokens=2048``). The download is best-effort: if no # progress exists yet, the file is simply absent and the eval # starts from scratch. huggingface-cli download \ --repo-type dataset \ --local-dir /home/user/output \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v1 \ sft-eval-progress.jsonl 2>/dev/null \ && mv /home/user/output/sft-eval-progress.jsonl \ /home/user/output/sft-eval-report.json.progress.jsonl \ || echo "(no prior progress JSONL — fresh eval start)" CMD=( "python" "-m" "training.evaluate_sft" "--adapter-repo" "LevArtesa/sft-humanizer-de-v1-lora" "--holdout-path" "/app/dataset/holdout.jsonl" "--report-path" "/home/user/output/sft-eval-report.json" "--max-new-tokens" "1024" ) ;; evaluate_sft_v2) # Hold-out evaluation of the SFT v2 adapter against the v2 hold-out # JSONL (deterministic seed=42, 40 records). # Pre-fetches the v2 hold-out and any prior progress JSONL so the # eval is resumable across restarts. Targets # ``LevArtesa/sft-humanizer-de-v2-lora`` (R14.1, R14.2). mkdir -p /app/dataset if [ ! -f /app/dataset/holdout-v2.jsonl ]; then echo "Downloading holdout-v2.jsonl from LevArtesa/sft-humanizer-dataset-v2 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v2 \ holdout-v2.jsonl fi mkdir -p /home/user/output huggingface-cli download \ --repo-type dataset \ --local-dir /home/user/output \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v2 \ sft-eval-v2-progress.jsonl 2>/dev/null \ && mv /home/user/output/sft-eval-v2-progress.jsonl \ /home/user/output/sft-eval-v2-report.json.progress.jsonl \ || echo "(no prior v2 progress JSONL — fresh eval start)" CMD=( "python" "-m" "training.evaluate_sft" "--adapter-repo" "LevArtesa/sft-humanizer-de-v2-lora" "--holdout-path" "/app/dataset/holdout-v2.jsonl" "--report-path" "/home/user/output/sft-eval-v2-report.json" "--max-new-tokens" "1024" ) ;; validate_dataset) CMD=("python" "/app/scripts/validate_dataset_with_gptzero.py") ;; rebuild_dataset) CMD=("python" "/app/scripts/rebuild_dataset_with_gptzero.py") ;; rebuild_dataset_aggressive) # Aggressive-prompt rebuild driver around the GPTZero validator. # Pre-fetches the cleaned v2 input JSONL and any prior rebuild # checkpoint so a Space restart resumes from where it left off. # The --aggressive-prompt flag is appended to CMD so the rebuilder # swaps the production system prompt for _AGGRESSIVE_SYSTEM_PROMPT # (see scripts/pilot_rebuild_test.py). HF_TOKEN is required since # both dataset repos are private. mkdir -p /app/dataset if [ ! -f /app/dataset/v2-clean.jsonl ]; then echo "Downloading v2-clean.jsonl from LevArtesa/sft-humanizer-dataset-v1 ..." huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v1 \ v2-clean.jsonl fi # Best-effort pre-fetch of any prior rebuild checkpoint so a # restarted container resumes from where it left off. If no # checkpoint exists yet on the dataset repo, the rebuilder simply # starts fresh — the missing-file error is swallowed. huggingface-cli download \ --repo-type dataset \ --local-dir /app/dataset \ --local-dir-use-symlinks False \ LevArtesa/sft-humanizer-dataset-v2 \ rebuild-checkpoint.json 2>/dev/null \ || echo "(no prior rebuild checkpoint)" CMD=("python" "/app/scripts/rebuild_dataset_with_gptzero.py" "--aggressive-prompt") ;; *) echo "Unknown ENTRYPOINT_MODE='$MODE'. Falling back to 'serve'." >&2 CMD=("python" "-m" "training.serve") ;; esac # --dry-run handling: if first arg is --dry-run, just print and exit 0 if [ "${1:-}" = "--dry-run" ]; then shift printf "%s " "${CMD[@]}" printf "%s " "$@" echo exit 0 fi exec "${CMD[@]}" "$@"