Releases: funakoshi-takehiro/library-hiroba
Releases · funakoshi-takehiro/library-hiroba
Release list
v0.7.0
Add Qwen3.5 0.8B and Qwen3 4B, and let the heavy one stay off auto Two models the browser can actually run have appeared since the model table was last surveyed. Both are added with their revisions pinned. qwen35_08 is the real find: 469MB in the browser, which is lighter than the current default (900MB) and lighter than qwen3_06, the entry added specifically to rescue low-memory devices — while being two generations newer. Loaded through our own Colab path it answers 富士山 and 0℃ and carries a name across turns; it writes 冰点 with the Chinese character now and then, which is worth knowing before it reaches a classroom. qwen3_4b is the answer to "something smarter", and it is 2.9GB per student — 116GB for a class of forty. It is listed and loadable by name, but auto and recommend() will not choose it: whether a school's network can carry that is a judgement for someone who knows the room. The reason string had to change too, because "the smartest in the list" stops being true once something heavier is in the list but out of reach of auto. The same-model guard fired on Qwen3.5-0.8B-Text-ONNX. Rather than widen it on a hunch, the two configs were compared: eight text-architecture fields identical and a byte-identical chat template (sha256 273d8e0e…), so -Text is a format suffix like -ONNX and is now stripped as one. transformers floor moves to 4.57, the first stable release that can read model_type qwen3_5. The browser side does not work until the host adds both to its own list — PYHIROBA_MODELS_REQUEST.md asks for that, and records what was checked and ruled out: Qwen3 8B ships only in ONNX Runtime GenAI layout, Qwen3.5 2B and 4B exist only as vision models, and LLM-jp has no ONNX above 150M anywhere on the Hub, so it cannot be broadened at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nmuk8vkLHike1JrLcukFwJ
v0.6.2
Release 0.6.2 and bring the status page up to date Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nmuk8vkLHike1JrLcukFwJ
v0.6.1
Tell them what the pre-release audit turned up Six ways to get a wrong answer silently, and one of them was in the check promised last time: the dimension guard read only the first vector, so a response that changed partway through passed and dot() then compared prefixes. That one can reorder results in a lesson already vendored, so the note says which single finding is worth pulling forward and which are only about refusing bad input. Also records the lock mistake as mine — reusing the generation lock would have parked embed() on the event loop behind a running generation — and lists what was checked and found clean, so the absence of an HTML path for host data is on the record rather than assumed.
v0.6.0
Add ai.embed and ai.search for meaning-based lookup
The host's ai-embed bridge is in place, so this is the Python entry point
students write against: embed() turns text into a vector, search() ranks
documents by meaning. A string in gives one vector back, a list gives a
list in the same order, and vectors arrive L2-normalised so a dot product
is already the cosine.
Embedding models live in EMBED_MODELS, not alongside the chat ones. Put
them in MODELS and three things break at once: models() advertises a
model you cannot generate with, load("minilm") succeeds and leaves a
non-generative model in _pipe for ask() to choke on, and recommend()
becomes free to suggest it. Tests hold the separation.
The host refuses more than 256 texts, and passing that through would mean
the same notebook failing in PyHiroba and succeeding in Colab — the exact
divergence this library exists to prevent. embed() splits into batches
itself, so the limit is invisible. search() sends the query and documents
in one call rather than two, since each round trip is a bridge crossing.
Colab pools and normalises against transformers and torch directly.
sentence-transformers wants transformers>=5 and brings scikit-learn and
scipy along for fifteen lines of arithmetic, and writing the same steps
the host writes is what keeps the two paths meaning the same thing. The
masked mean is checked numerically against a hand-computed reference:
padding carries an extreme value, so a mean that forgot to mask would be
nowhere near the expected answer.
A vector that comes back unnormalised would not raise anything — it would
just quietly reorder results — so one length is checked per call.
v0.5.4
Say so when the published version predates the fix The notebook installs from PyPI, PyPI is still on 0.5.3, and the timeout that ends the eternal spinner is in 0.5.4 — which was never tagged, so it was never published. Running the notebook fetched code that still has the unbounded read. Nothing had changed because nothing could have. The setup cell now knows which version the notebook needs and says when what it got is older, along with the command to install from main. Before this it printed "library-hiroba 0.5.3" and left the reader to work out that 0.5.3 was the problem — which is not something a reader can know. Checked against the five states this cell can find itself in: published version too old, current, stale copy still in memory, second run in one session, and PyHiroba, where a vendored copy has no metadata to compare.
0.5.4
Bound the wait for a token, and never generate twice at once The screenshot finally separated the layers. The typed message and the spinner both rendered, and the diagnostic cell answered in 2.9 seconds on the same kernel — so display, threading, model and memory all work. Only the form's generation never came back. TextIteratorStreamer was built without a timeout, and its __next__ blocks on an empty queue forever. If generation goes quiet the read never returns, and the spinner cannot be escaped by construction. Removing the timeout again reproduces the eternal spinner locally, which is the first time this failure has been visible outside Colab. The wait is now polled in short slices with a total budget, rather than one long block. Same deadline, but no thread parked for minutes — a blocked default-executor thread is joined at interpreter exit, so the long form turned a regression into a hang even in the test suite. Now it fails in twenty seconds and says what it was waiting for. The screenshot also caught two generations running at once: the form's submission was still in flight when the diagnostic cell called the same pipeline. transformers pipelines are not thread-safe, and that is a plausible way for the first one to have been left wedged. A second concurrent generation is now refused outright instead of quietly corrupting both, with the lock released in a finally so a crash cannot lock the model away for the rest of the session. The waiting bubble counts the seconds. A spinner that never changes is indistinguishable from a dead one, which is exactly how this was read each time it was reported.
v0.5.3
Show what you typed before the answer comes back The form reaches the display now — the spinner proves it — but the first thing Talk.stream yielded was the first non-empty fragment of the reply. Until the model produced something there was nothing on screen but a spinner, in a chat, where your own message is the one thing that should never be in doubt. It now yields the conversation as soon as the message is in it, with the spinner sitting in the assistant's bubble where a messenger would put it. That also makes the wait legible, which it wasn't. A spinner said the same thing whether the model was mid-sentence or three minutes into a 1.5GB first download, so a slow load was indistinguishable from a hang — which is how it was read. Ai.is_loaded() separates them and the waiting bubble says which one is happening. An answer that comes back empty used to leave an empty bubble, equally unreadable. A thinking model that spends its whole budget inside <think> does exactly that. It now says so and suggests raising max_tokens. None of this explains a wait that never ends, and I cannot reproduce one here. check_ai_colab.py --chat drives the same stream outside the form and prints each update with its elapsed time, so the next report can say whether it stalls in load or in generation.
v0.5.2
Ask the host what it supports instead of assuming Forms landed in PyHiroba on 2026-08-09, and Talk.form() kept warning that they do not work there. The gate was in_browser(), which only reports whether js.pyhirobaAsk exists — that answers "can this host run a model", never "can this host return a form value". Those were the same question right up until they weren't, and the warning then sat on top of a form that worked, telling teachers the opposite of the truth. It was enough to hold the vendored copy back at 0.4.0. The host now publishes what it implements as pyhirobaFeatures, and the gate reads that. Matching is whole-token against the comma list rather than substring, so ai-probe alone cannot pass for ai. A host that publishes nothing is treated as old-but-unknown, not as feature-less: ai.stream() still probes ai-ask-start and falls back if it is refused, exactly as before. Only a host that does name its features is taken at its word, which spares it a round trip that is certain to fail. That distinction is what keeps a host which implemented streaming before the feature string existed from being demoted. Docs said forms do not work in PyHiroba in four places, which is now false everywhere it appeared. FORMS.md and the request letter are marked resolved and kept as the record — including which of the four open decisions went which way. Streaming stays unimplemented on the host side and needs nothing from us; it degrades to whole-answer replies as designed. Adding ai-stream to the feature string is all that will be required to switch it on.
v0.5.1
Run the form's work on its own loop, not Colab's The diagnostic settled it. In Colab the click handler runs, and all three ways of writing to an Output widget reach the screen — so neither the callback nor the write was ever the problem. What never happened was the work itself: a coroutine handed to ensure_future from that callback sits in the queue and is never executed. Colab does not pump the event loop between cell executions, and nothing about the loop says so — asyncio.get_running_loop() returns it reporting running=True, which is why this read as a display bug for so long. The field cleared because that happens synchronously in the handler; the answer never came because the coroutine producing it never started. So the widgets path now runs its coroutine on a thread with a loop of its own, which does not care whether the host is pumping anything. Writing through `outputs` is what makes this safe from another thread: it is a traitlet assignment over the comm, with none of display()'s dependence on the executing cell. The other two paths keep their existing behaviour. The test pins the property that actually matters: press submit, then give the loop no further turns at all — the tests run the whole thing inside asyncio.run and let it exit — and the answer still has to arrive. Restoring ensure_future fails it. threading joins the ui allowlist for this. It is imported inside the function and only on the ipywidgets path, so PyHiroba, which has neither IPython nor ipywidgets, never reaches it and never asks Pyodide for a thread. Noted in the integration doc as that guard demands.
v0.5.0
Put the chat boilerplate behind ui.conversation and ai.talk
Writing chat.ipynb made it obvious that the interesting part of a chat
lesson is about four lines, and the other forty-five are the same every
time. That boilerplate was already copied into five places — three in the
README, one in each notebook — so fixing it meant fixing it five times.
Two pieces, split by what they need. ui.conversation() holds a
conversation and renders it; say/reply/note replace hand-built
{"role": ..., "content": ...} dicts, and it can be displayed while still
empty, which ui.chat cannot. Display stays in Chat — Conversation
delegates to it rather than growing a second copy of the markup.
ai.talk() sits on top and adds what only matters when the other side is a
model: folding recent turns back into the prompt, since ai.ask() keeps
nothing between calls; cutting the answer where a small model starts
writing your next line for you; and rebuilding the bubbles as tokens
arrive, without letting the half-written text into the history where it
would poison the next prompt. It lives in _ai.py and reaches for ui only
when rendering, the same way Recommendation does, so ui still knows
nothing about ai.
talk.form() owns both the field and the handler, so the field name and
the argument name can no longer drift apart — a mismatch there breaks
only the form, and only in a notebook. Since forms still do nothing in
PyHiroba, it says so there instead of rendering a dead input.
The README's AI chat examples were the ones with no memory at all: their
history fed the display while ai.ask() saw only the latest question.
Rewriting them closes that quietly along the way.