Skip to content

Make the project runnable end to end, add tests, docs and a browser demo - #1

Merged
Ssavan99 merged 9 commits into
mainfrom
feat/reproducible-pipeline-and-demo
Aug 16, 2026
Merged

Make the project runnable end to end, add tests, docs and a browser demo#1
Ssavan99 merged 9 commits into
mainfrom
feat/reproducible-pipeline-and-demo

Conversation

@Ssavan99

Copy link
Copy Markdown
Owner

The repo held solid modelling work — 90.2% test accuracy over 80 MODI + Devanagari classes — behind a one-line README and three notebooks that read data/, data2/ and .npz bundles that are not in the repo. Nothing was runnable from a clone.

The unlock

The label→index mapping was never saved with the models, which would normally make the committed weights useless. It turned out to be recoverable: training used LabelBinarizer, which sorts alphabetically, so the class list is just the sorted character column of the committed CSVs.

Verified — Model 1 classifies 46 of the 47 committed sample glyphs correctly (97.9%). That makes the repo fully runnable and testable with no dataset download.

What changed

Hygiene

  • Fixed the activation-maximization cell that shipped a traceback: it passed a layer name string as seed_input, so tf-keras-vis died on Cast string to float is not supported. Now a (1,32,32,3) tensor; verified to run, output committed as a figure.
  • Removed unused imports, dead commented-out blocks, and a pip log that leaked local Anaconda paths into committed output.
  • Deleted custom_cnn_model_2.zip (a redundant copy of the folder beside it) and an orphan image.
  • .gitignore now excludes the datasets.

Runnable

  • src/ — class-map recovery, prediction CLI, feature-map CLI, weight export.
  • requirements.txt pinned to versions actually verified.
  • docs/DATA.md — dataset provenance, expected layout, how to regenerate the bundles.

Tested

  • 19 tests over committed artifacts alone. pytest -q exits 0 in ~50s on CPU.
  • tests/test_js_parity.py re-runs the exported op plan in NumPy and asserts it matches Keras to 1e-4, so the browser demo cannot silently drift from the trained model.
  • GitHub Actions workflow.

Browser demo (docs/)

  • Static GitHub Pages page, no build step, no CDN, no network calls after load. Model 2's weights export to a flat float32 blob plus a JSON op plan; the forward pass is hand-written in JS.
  • Gallery of the 47 real MODI glyphs as the primary input — drawing was deliberately demoted to secondary, since nobody knows MODI glyph shapes and freehand input is out of distribution.
  • Layer-by-layer feature-map explorer and an occlusion-sensitivity heatmap.

README — rewritten from one line, with verified commands, real numbers, figures, and honest limitations including the multi-label stroke experiment that failed at ~51% (chance).

Review

/code-review at high level found 4 issues; 3 were real code bugs and are fixed in 923f6ef:

  • the cached class list was mutable and shared, so a caller appending to it silently mislabelled every later prediction
  • Flatten was declared exportable but emitted a parameterless stub the JS runtime cannot execute
  • --topk 0 crashed on max() of an empty sequence

The fourth was a risk flag on whether the TF 2.9.1 pin set resolves on Linux/Python 3.10 — this PR's CI run is what settles it.

Notes

  • Nothing here retrains anything; all reported numbers come from the existing notebook outputs.
  • GitHub Pages needs pointing at docs/ once this merges.

… artifacts
- character_label_classification.ipynb: the activation-maximization cell passed a
layer NAME string as seed_input, so tf-keras-vis died on
'Cast string to float is not supported'. seed_input is now a (1,32,32,3) tensor
matching the model input. Verified to run; output saved as
Images/model2_activation_maximization.png.
- Drop unused imports (MobileNet, VGG16, confusion_matrix, ConfusionMatrixDisplay),
duplicate numpy/matplotlib imports and commented-out cruft.
- Clear the pip log that leaked local Anaconda paths into committed output.
- Delete custom_cnn_model_2.zip (redundant copy of custom_cnn_model_2/) and the
orphan Images/model2_prediction-2.png.
- .gitignore: exclude data/, data2/, *.npz, *.zip, *.rar.
- Add SYNC.md documenting local vs origin state.
The notebooks read data/, data2/ and .npz bundles that are not in the repo, so a
clone could not run anything. The committed SavedModels plus feature_data/ are
enough for inference - the missing piece was the label->index map, which was
never saved.
It is recoverable: training used LabelBinarizer, which sorts alphabetically, so
the class list is just the sorted character column of the committed CSVs.
Verified at 46/47 on the sample glyphs.
- src/classes.py derive both class lists (47 MODI, 80 combined) from the CSVs
- src/model.py SavedModel loading + the notebooks' image preprocessing
- src/predict.py CLI: python -m src.predict <image> [--model N] [--topk K]
- src/featuremaps.py CLI: render conv activation grids, the repo's namesake
- src/export_weights.py flatten a model to float32 + a JSON op plan
- requirements.txt pinned to the versions actually verified
- docs/DATA.md dataset provenance, expected layout, how to regenerate
- tests/ 18 tests over committed artifacts only, no dataset needed
- .github/workflows/ci.yml run them on every push and PR
- classes.py: cache the class list as a tuple and hand out a fresh list per
call. The sequence order IS the model's output order, so a caller appending
to the shared list silently mislabelled every later prediction. Regression
test added.
- export_weights.py: drop Flatten from _SUPPORTED so an unimplemented layer
raises at export time instead of emitting a parameterless stub the browser
runtime cannot execute; validate layer activations against what cnn.js
implements, for the same reason.
- predict.py: reject --topk below 1 at the argparse boundary instead of dying
on max() of an empty sequence.
Replaces the single-line placeholder. Title, one-line pitch, what MODI is and
why recognising it matters, verified install/usage with real captured output,
results tables with the actual trained numbers, the feature-map and activation-
maximisation figures, repository layout, a note on how the class mapping was
recovered, and an honest limitations section covering the failed multi-label
experiment, the 32x32 downsampling, the uncontrolled model comparison and the
incomplete local dataset copy. No badges.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:923f6effc7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadsrc/export_weights.py
Comment on lines +33 to +35
# Layer classes the JavaScript runtime knows how to execute. Anything absent
# here raises at export time, where it is diagnosable, rather than producing a
# plan the browser cannot run. Keep this in sync with docs/cnn.js.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Commit the browser page and JavaScript runtime

The export explicitly targets docs/cnn.js, but a repo-wide git ls-tree -r/rg check of this commit finds no HTML or JavaScript files at all—docs/ contains only DATA.md and the weight artifacts. Consequently, GitHub Pages has no page to serve and nothing consumes model2.json, model2.bin, or samples.bin, so the advertised browser demo is unusable until its static page and runtime are committed.

Useful? React with 👍 / 👎.

Comment threadsrc/classes.py
Comment on lines +73 to +76
for _, row in df.iterrows():
# Characters shared between the two scripts collapse to one class, so
# keep the first row seen rather than letting the later one win.
table.setdefault(str(row["character"]), tuple(int(row[f]) for f in STROKE_FEATURES))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid silently choosing MODI strokes for combined labels

When Model 2 predicts a label shared by both scripts, this always retains the first (MODI) row. This produces incorrect explanatory output for a Devanagari tha: the existing CSV's MODI row at line 36 has strokes o and |, while its Devanagari row at line 81 has o, X, ^, and >. Since predict --model 2 prints this lookup as the predicted character's stroke primitives, ambiguous duplicate labels should be reported as such or handled without silently presenting the MODI vector as valid for both scripts.

Useful? React with 👍 / 👎.

Static page in docs/ - no build step, no CDN, no network calls after load.
Model 2's weights are exported to a flat float32 blob plus a JSON op plan and
docs/cnn.js runs the forward pass by hand; tests/test_js_parity.py already
asserts that plan matches Keras to 1e-4.
Design note: drawing is deliberately NOT the primary interaction. MODI glyphs
are unfamiliar to any visitor and freehand mouse input is out of distribution,
so leading with a blank canvas would both confuse people and understate the
model. Instead the 47 real committed glyphs are the gallery, and drawing is a
secondary, collapsed panel with a tracing guide and an explicit warning.
- Gallery of the 47 real MODI samples, decoded from weights/samples.bin
- Top-5 prediction with a match indicator against the true label
- Layer-by-layer feature-map explorer across all four conv layers
- Occlusion sensitivity sweep, 64 passes, chunked per animation frame
- Responsive, light/dark, keyboard accessible, reduced-motion aware
The two BatchNorm layers are folded into the following conv at load time -
algebraically exact for valid padding - which cuts a forward pass from ~177ms
to ~26-65ms and makes the occlusion sweep tolerable at ~4s.
Feature maps are genuinely sparse (often 20-35 of 64 channels silent for a
given glyph). The UI discloses the per-tile normalisation and the silent-channel
count rather than hiding it behind a prettier ramp.
Footer links to the repo absolutely: when Pages serves /docs that folder IS the
site root, so a relative ../ would leave the repo.
.bar__fill is a <span> and the stylesheet never set display:block, so it stayed
display:inline - width and height do not apply to inline boxes, and every bar
rendered 0x0. The amber fill and the correct percentage were both there in the
DOM, just with no layout box. Verified on the deployed page: the top bar now
renders 333px of a 358px track for a 92.9% prediction.
The README hero was captured before this fix, so it showed empty bars. Retaken.
Also make the footer repo link absolute rather than '../': when Pages serves the
/docs folder that folder IS the site root, so the relative link left the repo.
docs/cnn.js implements softmax only in dense(); its conv2d branches on relu and
copies everything else through unchanged. The exporter allowed softmax on any
layer, so a softmax convolution would have exported cleanly and then silently
emitted unnormalised logits in the browser while the UI presented them as
probabilities. Conv2D is now restricted to relu/linear, Dense keeps softmax.
Exported artifacts are byte-identical; no redeploy needed.
@Ssavan99

Copy link
Copy Markdown
OwnerAuthor

Phase 4 landed — the demo is live

https://ssavan99.github.io/FeatureExtraction_from_AncientScript/

Verified against the deployed URL with a clean browser profile: no console errors, three asset requests (model2.json, model2.bin, samples.bin) all 200 and none after load, the selected gallery glyph predicts its own label at 92.9%, feature maps render on all four conv layers, and the 64-pass occlusion sweep completes.

Two real bugs were caught reviewing the deployed page, not the source:

  1. .bar__fill is a <span> and the stylesheet never set display:block, so it stayed display:inline — width and height don't apply to inline boxes and every prediction bar rendered 0×0. The amber fill and the right percentage were both in the DOM with no layout box. Fixed in 77b6cb0; the top bar now renders 333px of a 358px track for a 92.9% prediction. The README hero had been captured before this fix and showed empty bars, so it was retaken.
  2. The footer's ../ repo link was wrong for this deployment: when Pages serves the /docs folder, that folder is the site root, so ../ left the repo entirely. Now absolute.

/code-review on the Phase 4 diff found one further latent issue — the exporter permitted softmax on any layer while docs/cnn.js only implements it in dense() (its conv2d branches on relu and copies everything else through), so a softmax convolution would have exported cleanly and then silently emitted unnormalised logits presented as probabilities. Fixed in 61578d0 by splitting the allowlist per layer type. Exported artifacts are byte-identical.

Note for whoever merges

Pages is currently serving /docs from this feature branch so the demo could be verified before merge. After merging, repoint the Pages source at main (Settings → Pages → Branch), or the site will freeze at this branch's state.

@Ssavan99
Ssavan99 merged commit 8d6df2f into mainAug 16, 2026
4 checks passed
@Ssavan99
Ssavan99 deleted the feat/reproducible-pipeline-and-demo branch August 16, 2026 01:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Ssavan99