This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle

What I Built

A website service for small businesses that will never write a design brief. The owner types one sentence. The goal is that a few minutes later they get a working, art-directed single page instead of a template that looks like every other template. Here's the pipeline:

"a page for my sewing and alterations shop in Kochi, hemming, blouse stitching and school uniforms" │ ▼ Gemma 4 E4B, on my laptop: writes the art-direction spec ▼ Qwen3-8B + a LoRA I trained on Tinker: writes 5 drafts (index.html, style.css, app.js) ▼ headless Chrome QA at 3 widths: JS errors, overflow, blank screens, a click-everything test ▼ Gemma again: fixes the JavaScript of drafts that crash ▼ Gemma again, with vision: scores the first screen of each working draft and picks one ▼ still weak? Gemma writes a fresh spec and it goes round once more a site, or an honest "this one didn't work" 

The people who'd pay are the ones in my test set: a dog-grooming van in Leeds, a grandmother's pickle business in Jaipur, a momo truck in Kathmandu, a one-person GST practice. They can describe the business in a sentence. They can't write "Fraunces at 4.7rem, #3b1ed2 ultraviolet, a clip-path wipe on scroll," and they won't pay an agency for it. So Gemma writes that part.

Where it stands, honestly. The pipeline works end to end: on 16 requests it had never seen, 14 got a working site. But working isn't the same as good. Looking at them myself, very few are pages a small business would pay for yet. Heroes are sparse, some booking forms don't do anything, and a few have copy mistakes a client would spot in seconds. For $10 of training credit I think that's a decent start, and the rest of this post is what I learned getting there and what I'd change.

The side-hustle math, once the quality is there. A finished site already costs only about $0.03 of Tinker sampling (five drafts), plus about 8 minutes on a laptop with a 4 GB GPU, where Gemma and the QA renders run at no cost per site. Training the model I kept cost $4.05. The cost side of the business works today. The quality side doesn't yet, so I'm not charging anyone.

Demo

Open the gallery of all 16 held-out requests: each one-liner, the spec Gemma wrote, the live site it became (click through), the QA verdict and Gemma's score.

The site kept for each of the 16 held-out requests, labelled with its QA result and Gemma's score

One request end to end: the Kochi tailor above took about 8 minutes and $0.02. Two of five drafts passed QA, and Gemma scored the best one 6/10, "Tailoring done slowly", fine but plain. With the bar raised to 7, the pipeline asked Gemma for a fresh spec, drew 3 more drafts for $0.01, and kept one Gemma scored 8/10: "Stitched with care, for over 30 years." It still has a slip a person would catch: its tagline reads "measure once, cut twice."

First pick for the Kochi tailor, scored 6/10: a plain page headed 'Tailoring done slowly'
First spec, best of five drafts: 6/10.

After a fresh spec, scored 8/10: a dark page with the large serif headline 'Stitched with care, for over 30 years.'
Fresh spec, three more drafts: 8/10.

Code

GitHub logo KunalSiyag / fine_tune_model

This repo is for my personal learning about fine tuning.

The interesting files are in dataset/pipeline/:

  • make_site.py: the whole product, one sentence in, a site out.
  • train_tinker.py: dry-run pricing, SFT and DPO on Tinker, best-of-k sampling.
  • brief_gemma.py: the spec writer and its checker.
  • pick_best.py: QA verdicts plus the Gemma vision judge.
  • fix_fonts.py and render.py: the QA tooling.

How I Built It

1. A dataset that teaches taste, not volume

I built 627 pages between Oct 11 and Oct 14, all checked before any training. Week 2 opened on Oct 12, so the first day of that work came just before the window. The training, the Gemma pipeline and every number in this post came after, on Oct 17 and 18. The pages are multi-page sites, single pages, and MIT-licensed Codrops demos. Every one passes render.py: no console errors, no horizontal overflow at 390 px, nothing that disappears under prefers-reduced-motion. From those come 1,146 SFT records in three prompt styles for the same page: a terse one-liner, a client brief, and a full art-direction spec. Because the same page exists as a one-liner and as a spec, I had real one-liner → spec pairs to show Gemma later.

2. Training Qwen3-8B on Tinker for about $4

My laptop has an RTX 3050 with 4 GB, and the records average about 13k tokens, so local training was out. Tinker runs LoRA training remotely: I write the loop in Python, their GPUs do the forward and backward passes.

I had $10 of credit, so the first thing I wrote was a dry-run that tokenizes every record with the real chat template and prices the run before spending anything:

sft_train 819 rows 9.28M tokens p50 12220 max 16366 dropped 327 SFT train $ 4.08 

I trained one epoch on the 819 records that fit in 16K tokens. Loss is only on the assistant's files, and it ends on <|im_end|> so the model learns to stop. The run took 71 steps on 9.20M tokens, about $4.05. Held-out NLL went 0.809 → 0.636 → 0.595 → 0.577, with training loss ending just above it.

3. Three jobs for Gemma 4

Writing the spec. brief_gemma.py sends the one-liner to gemma4:e4b with three real terse → spec pairs from the training set, a different trio for each request. My first version lost to raw one-liners 0 to 3 on the QA checks: it named fonts that aren't on Google Fonts, and its specs all came out terracotta-and-cream because it copied the mood of three fixed examples. Rules in the prompt weren't enough. Even after "repeat its place names word for word", it dropped "Lisbon". What worked was a checker that sends the spec back with the problem named:

def problems(request: str, spec: str) -> list[str]: """Checks prompt wording didn't reliably enforce: client's proper nouns kept, real Google Fonts only.""" nouns = {w.strip(",.") for w in request.split()[1:] if w[:1].isupper() and w.lower() != "i"} out = [f"keep the client's name '{n}' word for word" for n in sorted(nouns) if n not in spec] out += [f"'{m.group(0)}' is not a Google Fonts family; name a real one (e.g. IBM Plex Mono, Space Mono)" for m in GENERIC_FONTS.finditer(spec)] return out 

On my 8 development requests, the fine-tuned model finished 22 of 24 drafts from the fixed specs, against 15 of 24 from the raw one-liners. A small model with a detailed plan wanders off into loops much less often.

Repairing JavaScript. Almost every failing draft fails on a runtime error, usually Cannot read properties of null. When no draft passes, Gemma gets index.html, app.js and the browser's errors, and returns a fixed app.js. A fix often exposes the next error, so each round starts from the previous repair. One hint mattered: const top at the top level of a classic script clashes with window.top, and Gemma doesn't know that until the prompt says so.

Judging taste. QA can tell a page works, not that it looks good. Gemma 4 reads images, so it scores the first screen of each working draft from 1 to 10 and the best one is kept. Before trusting it I checked it against my own eye: the three pages I'd call good scored 7, 7 and 7, and the three I'd call weak scored 1, 5 and 4.

4. Measuring on requests nothing was tuned on

All my fixes were made while looking at the same 8 development requests, so I wrote 16 new ones before seeing any output and never tuned on them:

Requests with a working site
First draft only 7 / 16
Best of 5 drafts 12 / 16
+ Gemma JS repair 14 / 16
…and judged good by Gemma (≥ 6/10) 9 / 16

That last row is generous. Gemma's judge is a useful tiebreaker between drafts, but it scores "not broken" too kindly. By my own eye the bar a paying client would set is much higher, and few of the 16 clear it.

For the 5 weak requests, 3 more drafts from the same spec didn't help: still 0 judged good. A fresh spec plus just 3 drafts got 3 of the 5 judged good by Gemma (6, 6 and 7 out of 10), for $0.08. One got worse and one failed outright, so with 5 requests this is a strong hint, not a result. The bottleneck for taste is the spec more than the drafting, so the pipeline now re-specs weak requests by default.

5. A bug in my own dataset, and a fix that made things worse

Most failures were scripts looking for an element that doesn't exist. Then I noticed the file order. In all 722 training records that have both, the JavaScript comes before the HTML. My dataset builder emitted shared files first, so the model learned to write its script before the markup it controls, and then guess the IDs.

I tested the theory cheaply by starting the model's reply with <file path="index.html">. Among drafts that finished, "element not found" errors fell from 32% to 19%. But only 26 of 48 drafts finished at all, against 40 of 48 normally.

So I rebuilt the dataset with HTML first (byte-identical otherwise, which I checked) and spent $2.11 continuing the training for half an epoch. It was worse on every measure:

same 16 requests sft-v1 (shipped) sft-v2 (HTML first)
drafts passing QA 38% 21%
drafts that finish 83% 71%
stuck in loops 17% 29%
"element not found" errors 32% 37%

I kept sft-v1. A half-epoch at that learning rate may have disrupted the model more than the new order helped, and one run can't separate the two. That $2.11 bought a result I can report honestly instead of a model I could ship.

6. Smaller lessons

  • Lower temperature made it worse. At 0.4 instead of 0.7, 21 of 24 drafts never finished. They got stuck counting (--ease-out-back-166, -167, -168…). The longest numbered run in my training data is 19 lines, so the data didn't teach it.
  • My verifier had bugs too, as in Our verifier was buggier than the code it verified. A blank page passed because render.py counts text in the DOM, so I added a pixel check. A good page failed because the click test opened a mailto: link. Screenshot crashes were the harness, not the pages.
  • fix_fonts.py was the best $0 I spent. The model asks Google Fonts for axes a family doesn't have (Montserrat:wdth), Google answers 400, and the page fails QA. Asking Google which spec it accepts turned three failing development requests into working ones.

7. What "passes QA" doesn't mean

Before publishing the gallery I read every site. QA catches crashes, not dead features. Several booking forms and calculators on passing pages do nothing when used: their scripts look for an element that isn't there and quietly return. Some copy is wrong. The Spanish level quiz marks wrong answers as correct, the Dutch repair café turned into a coffee-machine repair shop, and Gemma's spec for the tea-estate wedding venue never mentioned weddings. A paying customer would need a person to check the page before it goes live. In the published copies I replaced phone numbers with zeros (an invented number can still be someone's), removed one street address, and turned down one page's full-volume white-noise button.

8. The difficulties, in one place

  • $10 forced hard cuts. One epoch instead of two or three, and the 327 longest records dropped to fit 16K tokens. Those were the richest, most complete pages in the dataset, the ones that teach craft.
  • An 8B model learns the format before the craft. After one epoch the model reliably writes index.html + style.css + app.js, named palettes and font pairings. It doesn't yet reliably compose a striking first screen or wire every interaction. Held-out loss was still falling when the credit ran out.
  • Small models loop. Counting runs (-166, -167, -168…) ate whole drafts, and lowering the temperature made it worse.
  • My own dataset had a bug (JavaScript before HTML), and the fix I could afford made the model worse.
  • Checking taste is hard. QA catches crashes, not dead buttons or bland pages. The vision judge helps pick between drafts, but it isn't a client.
  • Every number needed a second look. A blank page passed QA, a good page failed on a mailto: link, and Wi-Fi drops looked like page bugs. I re-ran anything that looked like the harness rather than the page.

Why Does Open Innovation Matter?

  • I could fine-tune for a niche. "Single pages with real art direction" is a style, and a LoRA on 627 curated pages teaches a style. I own that adapter, and a general-purpose API doesn't come with one.
  • The per-site cost is why this can become a side hustle. Five drafts cost about $0.03 on Tinker. Gemma writes, repairs and judges on a 4 GB laptop GPU, so the retries that make the output good cost nothing per attempt.
  • I could look inside every failure. Loss masks, sampling temperature, the exact tokens of a loop, the file order in my own data. None of that debugging is possible against a closed API.

How I Worked

I built this with Claude Code as a pair: it wrote much of the pipeline code and ran the experiments with me. The dataset decisions, the budget, and the choice of models were mine.

My First User

I don't have one yet, on purpose. The 16 requests in the gallery and the Kochi tailor are ones I wrote myself, not customers, and nobody has paid. I'd rather put it in front of a real shop owner once the pages are good enough to charge for than ask someone to pay for what it makes today. When I do, the question is simple: would they pay for the page they get?

Prize Categories

  • Overall
  • Best Use of Tinker: the whole fine-tune, from dry-run pricing to best-of-k sampling from the checkpoint, plus the retrain that told me what not to ship.
  • Best Use of Gemma: Gemma 4 E4B as the local spec writer, JavaScript repairer and vision judge, each with its own check.

What's next

  • Keep training, probably on Colab. Because the weights and data are mine, I'm not tied to one provider. Colab's GPUs are smaller than Tinker's, so the plan is QLoRA and more epochs on the records that fit. Whether the long records I dropped fit on a 16 GB card is something I'll have to test.
  • Train on the failures. DPO pairs where rejected is the same page with a real JavaScript bug or a bland first screen, since those are what the model gets wrong now.
  • Better checks. A test that clicks through every form and booking widget, a checker for key nouns ("wedding"), and a person reviewing each page before it goes to anyone.
  • Then a first user, and a price.