Skip to content

Releases: funakoshi-takehiro/library-hiroba

v0.7.0

Choose a tag to compare

@funakoshi-takehiro funakoshi-takehiro released this 04 Sep 19:35
df3c25e
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

Choose a tag to compare

@funakoshi-takehiro funakoshi-takehiro released this 04 Sep 19:02
f0f0436
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

Choose a tag to compare

@funakoshi-takehiro funakoshi-takehiro released this 20 Aug 00:41
533aae3
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

Choose a tag to compare

@funakoshi-takehiro funakoshi-takehiro released this 19 Aug 23:26
44f5590
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

Choose a tag to compare

@funakoshi-takehiro funakoshi-takehiro released this 12 Aug 08:14
e538c3d
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

Choose a tag to compare

@funakoshi-takehiro funakoshi-takehiro released this 12 Aug 07:52
bea218a
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

Choose a tag to compare

@funakoshi-takehiro funakoshi-takehiro released this 11 Aug 00:49
4a33346
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

Choose a tag to compare

@funakoshi-takehiro funakoshi-takehiro released this 10 Aug 14:31
cf66985
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

Choose a tag to compare

@funakoshi-takehiro funakoshi-takehiro released this 10 Aug 10:00
ccfdfa3
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

Choose a tag to compare

@funakoshi-takehiro funakoshi-takehiro released this 10 Aug 03:21
69fe8c2
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.