🎭 Theater & Live Performance · orchestra pit balance

Pit Mix LoRA

Fine-tune Qwen/Qwen3.5-9B on 80 mix notes so engineers balance pit ensembles.

SFT · Instruction· supervised fine-tune
Section · Tinker

The kernel.

full primer →

A LoRA fine-tune on 40–100 example completions teaches a small open model the exact way directors handle orchestra pit balance — so any downstream app can call the checkpoint and get on-house answers without a mega-prompt.

Why this primitivesft-instruct learns balance language via (prompt, completion) cross_entropy.

Kernel
Supervised fine-tuning on `(prompt, completion)` pairs. `service_client.create_lora_training_client(base_model=...)` → `forward_backward_async(data, 'cross_entropy')` → `optim_step_async(AdamParams(lr=1e-4))` → `save_weights_and_get_sampling_client(...)`. Teach a small open model to imitate a persona, a house style, or a domain of expertise.
Drives the UI as
a single-page report that plots the training loss curve and shows before-vs-after sample completions from the fine-tuned checkpoint
Appendix · Secrets

Required key.

TINKER_API_KEY
Single key for every Tinker post-training call — SFT, RL, DPO. Used ONLY at build time inside Lovable's Linux sandbox; the deployed Worker never touches Tinker.
open ↗

Add this in your Lovable project under Settings → Secrets before pasting the prompt below.

Appendix · Mega-prompt

The build prompt.

Paste into a fresh Lovable project. Make sure the key above is set first. read the build strategy →

Build "Pit Mix LoRA" as a ONE-SHOT Lovable build. The participant has only
5 credits — this single message must produce a working report page in one turn.

CONCEPT
Fine-tune Qwen/Qwen3.5-9B on 80 mix notes so engineers balance pit ensembles.
Discipline: Theater & Live Performance (orchestra pit balance).
Kernel: SFT · Instruction — supervised fine-tune.
Base model (Tinker id): Qwen/Qwen3-8B.
Why this kernel: sft-instruct learns balance language via (prompt, completion) cross_entropy.

SANDBOX-TRAINING PATTERN (READ THIS FIRST):
Tinker is Python-only. The deployed Lovable app runs on Cloudflare Workers,
which cannot run Python. The training script in this prompt therefore runs
ONCE, in Lovable's Linux build sandbox, DURING this build turn. It writes its
output to `src/data/run-artifact.json`. The deployed site imports that JSON
and renders it. Do NOT try to call `tinker` from a server function. Do NOT
`pip install tinker` inside `src/`. The training script lives at
`scripts/train.py` and is executed once, then its imports are gone.

SHARP EDGES (verified in production — the script below already handles all
three; do NOT "simplify" them away):
- `forward_backward` returns `metrics={"loss:sum": <raw sum>}`. There is NO
  `r.loss` attribute. Read `r.metrics["loss:sum"]` and divide by the
  supervised-token count for the batch to get a comparable per-token mean.
- Batch indexing MUST wrap with `%`. A slice like `data[a:b] or data[:4]`
  silently retrains the head of the dataset once indices run past the end.
- Use the `_async` SDK surface everywhere: `create_lora_training_client_async`,
  `create_sampling_client_async`, `forward_backward_async`, `optim_step_async`,
  `sample_async`, `save_weights_and_get_sampling_client_async`. Sync variants
  do not exist in the SDK version this pattern was validated against.
- Loss-curve SVG stroke: the shadcn/Tailwind v4 tokens in `src/styles.css` are
  defined as `oklch(...)` values, NOT raw HSL channels. Writing
  `stroke="hsl(var(--primary))"` on the `<path>` resolves to `hsl(oklch(...))`,
  which is invalid, and the line paints with no color (chart looks empty).
  Use `stroke="var(--primary)"` directly, or `stroke="currentColor"` on an
  element with `text-primary`. Use `strokeWidth={2.5}` with
  `strokeLinejoin="round"` for mobile legibility.

LOVABLE BUDGET (HARD CAP: ONE-SHOT, ~5 CREDITS TOTAL):
The participant has FIVE Lovable credits for the whole build. This prompt MUST
ship on the FIRST message with zero follow-ups. Engineer for that.
- ONE TanStack Start app, ONE route (`src/routes/index.tsx`). No nav, no auth.
- ONE data file: `src/data/run-artifact.json` — produced by running the
  Python training script BELOW in the sandbox during this build turn.
- ONE component surface: hero + hand-rolled SVG loss curve + sample gallery.
- NO database, NO Lovable Cloud, NO backend at runtime, NO auth, NO uploads.
- NO tests, NO docs pages, NO settings screens, NO theming toggles.
- Libraries: template defaults only. No chart lib — draw the loss curve as an
  SVG polyline.
- Keep the diff small enough to land in one build pass.

STEP 1 — write `scripts/train.py` exactly as below (adapt PAIRS / PROMPTS /
TRIPLES to real orchestra pit balance examples; keep 40–80 of them). Then run it once in the
sandbox with `python -m pip install -q tinker` + `python scripts/train.py`.
The script writes `src/data/run-artifact.json`.

```python
# scripts/train.py — run inside Lovable's Linux build sandbox
#   $ python -m pip install -q tinker
#   $ TINKER_API_KEY=... python scripts/train.py
# Produces src/data/run-artifact.json (loss curve + before/after samples).
#
# THIS SCRIPT ENCODES THREE VERIFIED FIXES — DO NOT REMOVE:
#   1. Loss is read from r.metrics["loss:sum"] (raw sum), normalized by the
#      supervised-token count for the batch. There is no r.loss attribute.
#   2. Batch indices wrap with % so partial batches never come through empty.
#   3. All SDK calls use the *_async variants.
import asyncio, json, pathlib, time, tinker
from tinker import types

BASE  = "Qwen/Qwen3-8B"
OUT   = pathlib.Path("src/data/run-artifact.json")
STEPS = 200
BATCH = 4
LR    = 1e-4
RANK  = 16

# DATASET — 40+ (prompt, completion) pairs for orchestra pit balance. REPLACE with real examples.
PAIRS = [
    {"prompt": "...prompt about orchestra pit balance...", "completion": "...the orchestra answer..."},
    # ~40 total — swap in your domain data before shipping.
]

async def main():
    t0 = time.time()
    svc = tinker.ServiceClient()                                       # reads TINKER_API_KEY
    train = await svc.create_lora_training_client_async(base_model=BASE, rank=RANK)
    tok = train.get_tokenizer()

    def make_datum(prompt: str, completion: str) -> types.Datum:
        p_ids = tok.encode(prompt, add_special_tokens=False)
        c_ids = tok.encode(completion, add_special_tokens=False) + [tok.eos_token_id]
        input_ids = p_ids + c_ids
        target    = input_ids[1:] + [tok.eos_token_id]
        weights   = [0]*len(p_ids) + [1]*len(c_ids)
        return types.Datum(
            model_input=types.ModelInput.from_ints(tokens=input_ids),
            loss_fn_inputs=dict(target_tokens=target, weights=weights),
        )

    data = [make_datum(p["prompt"], p["completion"]) for p in PAIRS]
    # supervised-token count per example (== len(completion_ids) + 1 for eos)
    sup_counts = [
        len(tok.encode(p["completion"], add_special_tokens=False)) + 1
        for p in PAIRS
    ]

    async def sample(client, prompt: str) -> str:
        pr = types.ModelInput.from_ints(tokens=tok.encode(prompt, add_special_tokens=False))
        r  = await client.sample_async(
            prompt=pr, num_samples=1,
            sampling_params=types.SamplingParams(max_tokens=140, temperature=0.7))
        return tok.decode(r.sequences[0].tokens)

    probes   = [p["prompt"] for p in PAIRS[:6]]
    baseline = await svc.create_sampling_client_async(base_model=BASE)
    before   = [await sample(baseline, q) for q in probes]

    losses = []
    for step in range(STEPS):
        start = (step * BATCH) % len(data)
        idxs  = [(start + k) % len(data) for k in range(BATCH)]
        batch = [data[i] for i in idxs]

        fb = await train.forward_backward_async(data=batch, loss_fn="cross_entropy")
        r  = await fb.result_async()
        # Tinker returns metrics={'loss:sum': X} — normalize by supervised tokens.
        loss_sum = float(r.metrics["loss:sum"])
        n_sup    = sum(sup_counts[i] for i in idxs)
        loss     = loss_sum / max(1, n_sup)
        losses.append([step, loss])

        opt = await train.optim_step_async(types.AdamParams(learning_rate=LR))
        await opt.result_async()
        if step % 10 == 0 or step == STEPS - 1:
            print(f"[tinker] step {step:03d}/{STEPS} loss={loss:.4f} t={time.time()-t0:.1f}s", flush=True)

    save  = await train.save_weights_and_get_sampling_client_async(name="final")
    after = [await sample(save, q) for q in probes]
    checkpoint = getattr(save, "checkpoint_path", None) or getattr(save, "path", None) or "tinker://final"

    OUT.parent.mkdir(parents=True, exist_ok=True)
    OUT.write_text(json.dumps({
        "idea": "theater-pit-mix-lora-15",
        "kernel": "sft-instruct",
        "base_model": BASE,
        "training": {"steps": STEPS, "lr": LR, "batch_size": BATCH, "rank": RANK,
                      "loss_curve": losses},
        "samples": [{"prompt": q, "before": b, "after": a}
                    for q, b, a in zip(probes, before, after)],
        "checkpoint": checkpoint,
        "wall_time_seconds": round(time.time() - t0, 1),
    }, indent=2))

asyncio.run(main())
```

STEP 2 — write `src/routes/index.tsx` exactly as below. It imports the JSON
artifact produced above and renders the report (hero + hand-rolled SVG loss
curve + before/after gallery). No runtime API calls, no server functions.

```tsx
// src/routes/index.tsx — renders the JSON artifact produced by scripts/train.py
import { createFileRoute } from "@tanstack/react-router";
import artifact from "@/data/run-artifact.json";

/** Built during the Tinker Folio Hackathon organised by StreetKode Fam during Indian Krump Festival 14 */
export const Route = createFileRoute("/")({
  head: () => ({ meta: [{ title: `Pit Mix LoRA — Tinker Folio` }] }),
  component: Report,
});

function LossCurve({ points }: { points: [number, number][] }) {
  if (!points.length) return null;
  const w = 800, h = 240, pad = 24;
  const maxLoss = Math.max(...points.map((p) => p[1]));
  const minLoss = Math.min(...points.map((p) => p[1]));
  const scaleX = (x: number) => pad + (x / (points.length - 1)) * (w - pad * 2);
  const scaleY = (y: number) =>
    h - pad - ((y - minLoss) / Math.max(1e-6, maxLoss - minLoss)) * (h - pad * 2);
  const d = points.map((p, i) => `${i === 0 ? "M" : "L"}${scaleX(i)},${scaleY(p[1])}`).join(" ");
  return (
    <svg viewBox={`0 0 ${w} ${h}`} className="w-full h-auto border border-border bg-card">
      <path d={d} fill="none" stroke="hsl(var(--primary))" strokeWidth={2} />
    </svg>
  );
}

function Report() {
  const a = artifact as typeof artifact;
  return (
    <main className="max-w-5xl mx-auto px-6 py-16">
      <span className="text-xs tracking-[0.28em] uppercase text-primary">{a.kernel} · {a.base_model}</span>
      <h1 className="font-serif text-4xl sm:text-6xl mt-3 italic">Pit Mix LoRA</h1>
      <p className="mt-4 text-muted-foreground max-w-2xl">Fine-tuned on {a.training.steps} steps. Report generated by the training script; no runtime API calls.</p>
      <section className="mt-10">
        <h2 className="text-lg uppercase tracking-[0.2em] mb-3">Loss curve</h2>
        <LossCurve points={a.training.loss_curve} />
      </section>
      <section className="mt-12 grid gap-6">
        <h2 className="text-lg uppercase tracking-[0.2em]">Before vs after</h2>
        {a.samples?.map((s, i) => (
          <article key={i} className="border border-border p-6">
            <div className="text-xs uppercase tracking-[0.2em] text-primary">Prompt</div>
            <p className="mt-1 whitespace-pre-wrap break-words">{s.prompt}</p>
            <div className="grid sm:grid-cols-2 gap-4 mt-4">
              <div><div className="text-xs uppercase text-muted-foreground">Base</div><p className="mt-1 text-sm whitespace-pre-wrap break-words">{s.before}</p></div>
              <div><div className="text-xs uppercase text-primary">Tuned</div><p className="mt-1 text-sm whitespace-pre-wrap break-words">{s.after}</p></div>
            </div>
          </article>
        ))}
      </section>
      <footer className="mt-16 text-xs uppercase tracking-[0.28em] text-muted-foreground">
        Built during the Tinker Folio Hackathon organised by StreetKode Fam during Indian Krump Festival 14
      </footer>
    </main>
  );
}
```

STACK
- TanStack Start, index route only. No auth, no DB, no Lovable Cloud.
- Tailwind + shadcn tokens; editorial look, gold accent on warm-cream.
- Footer renders the credit below (also present as JSDoc on the route).

KEY — only ONE secret is required at BUILD time:
1. `TINKER_API_KEY`. Sign up at https://tinker-console.thinkingmachines.ai,
   copy the key. This key is used by the Python training script that Lovable
   runs in its Linux build sandbox. The deployed Cloudflare Worker never
   touches Tinker — training already happened; the site renders the saved
   artifact. If you want to re-train later, re-run the script locally with
   `pip install tinker` and `export TINKER_API_KEY=...`.

BILLING GOTCHA (WILL BLOCK YOU IF SKIPPED):
Tinker rejects training with HTTP 402 "Access is blocked due to billing
status" when the account has no payment method attached — even after a
balance top-up. Before running the script:
  1. Visit https://tinker.thinkingmachines.ai/billing/balance
  2. Attach a card AND confirm account status shows active.
  3. Wait ~1–2 minutes for propagation.
If the script prints the 402, this is why. Do not add retry loops — fix
the account state.

CREDIT (must appear in the report footer AND as JSDoc on the route):
Built during the Tinker Folio Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Appendix · Market

Market sizing.

TAM
$30B
global live performance market
SAM
$700M
theatrical sound services
SOM
$14M
touring sound engineers

Indicative figures for hackathon pitches — refine with your own research before raising.

See also

Adjacent entries.