fix: image_generate renders inline + auto-title strips thinking preamble — v0.50.152 (closes #853, #857)
MEDIA: restore renders all https:// URLs as img (closes #853). _strip_thinking_markup strips Qwen3 plain-text reasoning preambles (closes #857).
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# Hermes Web UI -- Changelog
|
||||
|
||||
## [v0.50.152] — 2026-04-22
|
||||
|
||||
### Fixed
|
||||
- **Image generation renders inline** — `MEDIA:` token restore renders all `https://` URLs as `<img>`. (closes #853)
|
||||
- **Auto-title strips thinking preambles** — `_strip_thinking_markup()` strips Qwen3-style plain-text reasoning preambles. (closes #857)
|
||||
|
||||
## [v0.50.151] — 2026-04-22
|
||||
|
||||
### Added
|
||||
|
||||
@@ -70,6 +70,15 @@ def _strip_thinking_markup(text: str) -> str:
|
||||
s = re.sub(r'<\|channel\|>thought.*?<channel\|>', ' ', s, flags=re.IGNORECASE | re.DOTALL)
|
||||
s = re.sub(r'<\|turn\|>thinking\n.*?<turn\|>', ' ', s, flags=re.IGNORECASE | re.DOTALL) # Gemma 4
|
||||
s = re.sub(r'^\s*(the|ther)\s+user\s+is\s+asking.*$', ' ', s, flags=re.IGNORECASE | re.MULTILINE)
|
||||
# Strip plain-text thinking preambles from models that don't use <think> tags (e.g. Qwen3).
|
||||
# These appear as the very first sentence of the assistant response and are not useful as titles.
|
||||
s = re.sub(
|
||||
r"^\s*(?:here(?:'s| is) (?:a |my )?(?:thinking|thought) (?:process|trace|through)\b[^\n]*\n?"
|
||||
r"|let me (?:think|work|reason|analyze|walk) (?:through|about|this|step)\b[^\n]*\n?"
|
||||
r"|i(?:'ll| will) (?:think|work|reason|analyze|break this down)\b[^\n]*\n?"
|
||||
r"|(?:okay|alright|sure|of course),?\s+let me\b[^\n]*\n?)",
|
||||
' ', s, flags=re.IGNORECASE
|
||||
)
|
||||
s = re.sub(r'\s+', ' ', s).strip()
|
||||
return s
|
||||
|
||||
@@ -122,6 +131,7 @@ def _looks_invalid_generated_title(text: str) -> bool:
|
||||
or re.search(r'\b(they|user)\s+want(s)?\s+me\s+to\b', s, flags=re.IGNORECASE)
|
||||
or re.search(r'^\s*(i|we)\s+(should|need to|will|can)\b', s, flags=re.IGNORECASE)
|
||||
or re.search(r'^\s*let me\b', s, flags=re.IGNORECASE)
|
||||
or re.search(r"^\s*here(?:'s| is) (?:a |my )?(?:thinking|thought)", s, flags=re.IGNORECASE)
|
||||
or re.search(r'用户(要求|希望|想让|让我)', s)
|
||||
or re.search(r'请只?回复', s)
|
||||
or re.search(r'^\s*(ok|okay|done|all set|complete|completed|finished)\b[\s.!?]*$', s, flags=re.IGNORECASE)
|
||||
|
||||
@@ -717,7 +717,10 @@ function renderMd(raw){
|
||||
const base=document.baseURI.replace(/\/$/,'');
|
||||
src=src.replace(/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/i,base);
|
||||
}
|
||||
if(_IMAGE_EXTS.test(src.split('?')[0])){
|
||||
// MEDIA: tokens are only emitted for tool-generated images (image_generate etc.).
|
||||
// Render all https:// URLs as <img> — extension check would miss extensionless
|
||||
// CDN paths like fal.media content-addressed URLs (closes #853).
|
||||
if(_IMAGE_EXTS.test(src.split('?')[0]) || /^https?:\/\//i.test(src)){
|
||||
return `<img class="msg-media-img" src="${esc(src)}" alt="image" loading="lazy" onclick="this.classList.toggle('msg-media-img--full')">`;
|
||||
}
|
||||
return `<a href="${esc(src)}" target="_blank" rel="noopener">${esc(src)}</a>`;
|
||||
|
||||
99
tests/test_issues_853_857.py
Normal file
99
tests/test_issues_853_857.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Regression tests for #853 (image_generate inline rendering) and
|
||||
#857 (auto-title strips thinking preambles)."""
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
_SRC = os.path.join(os.path.dirname(__file__), "..")
|
||||
|
||||
|
||||
def _read(name):
|
||||
return open(os.path.join(_SRC, name), encoding="utf-8").read()
|
||||
|
||||
|
||||
# ── #853: MEDIA: URL restore renders any https:// as <img> ────────────────────
|
||||
|
||||
class TestMediaUrlRendersInline:
|
||||
"""The `MEDIA:` stash restore in renderMd() must render https:// URLs
|
||||
as <img> even when they lack a recognized file extension (FAL.ai and
|
||||
similar CDNs serve images via content-addressed paths)."""
|
||||
|
||||
def test_render_md_checks_https_scheme_for_img_tag(self):
|
||||
js = _read("static/ui.js")
|
||||
# The fix OR-chains the extension check with a scheme check so any
|
||||
# https:// URL in a MEDIA: token renders as <img>
|
||||
assert re.search(
|
||||
r"_IMAGE_EXTS\.test\(.*?\)\s*\|\|\s*/\^https\?:\\/\\/",
|
||||
js,
|
||||
), (
|
||||
"renderMd MEDIA: restore must accept any https:// URL as <img>, "
|
||||
"not only those with image file extensions (#853)"
|
||||
)
|
||||
|
||||
def test_img_class_applied_to_media_image(self):
|
||||
"""The resulting <img> uses the existing msg-media-img class so the
|
||||
styling/fullscreen-click interaction is preserved."""
|
||||
js = _read("static/ui.js")
|
||||
# The img tag is constructed with the existing class + onclick toggle
|
||||
assert "msg-media-img" in js
|
||||
assert "msg-media-img--full" in js
|
||||
|
||||
|
||||
# ── #857: thinking-preamble stripping in auto-title ──────────────────────────
|
||||
|
||||
class TestThinkingPreambleStripping:
|
||||
"""Qwen3 and similar models emit plain-text thinking preambles
|
||||
("Here's a thinking process:", "Let me think through this…") without
|
||||
<think> tags. These must be stripped before the text is used for
|
||||
session auto-titling."""
|
||||
|
||||
def test_strip_thinking_markup_drops_heres_thinking_process(self):
|
||||
from api.streaming import _strip_thinking_markup
|
||||
raw = "Here's a thinking process: 1. Analyze the request.\nThe answer is 42."
|
||||
out = _strip_thinking_markup(raw)
|
||||
assert "thinking process" not in out.lower()
|
||||
assert "42" in out, "Non-preamble content must be preserved"
|
||||
|
||||
def test_strip_thinking_markup_drops_let_me_think(self):
|
||||
from api.streaming import _strip_thinking_markup
|
||||
raw = "Let me think through this carefully.\nHere's the answer."
|
||||
out = _strip_thinking_markup(raw)
|
||||
assert "let me think" not in out.lower()
|
||||
|
||||
def test_strip_thinking_markup_drops_ill_think_about(self):
|
||||
from api.streaming import _strip_thinking_markup
|
||||
raw = "I'll think about this step by step.\nFinal result: 7."
|
||||
out = _strip_thinking_markup(raw)
|
||||
assert "i'll think about" not in out.lower()
|
||||
assert "result" in out.lower()
|
||||
|
||||
def test_strip_thinking_markup_drops_okay_let_me(self):
|
||||
from api.streaming import _strip_thinking_markup
|
||||
raw = "Okay, let me break this down.\nThe answer is yes."
|
||||
out = _strip_thinking_markup(raw)
|
||||
assert "okay, let me break" not in out.lower()
|
||||
|
||||
def test_strip_thinking_markup_preserves_non_preamble_content(self):
|
||||
"""When the text doesn't start with a thinking preamble, leave it alone."""
|
||||
from api.streaming import _strip_thinking_markup
|
||||
raw = "The user's question is about Python imports."
|
||||
out = _strip_thinking_markup(raw)
|
||||
assert "python imports" in out.lower()
|
||||
|
||||
def test_strip_thinking_markup_case_insensitive(self):
|
||||
from api.streaming import _strip_thinking_markup
|
||||
assert "Here's" not in _strip_thinking_markup("HERE'S A THINKING PROCESS:\nThe answer.")
|
||||
|
||||
def test_looks_invalid_generated_title_catches_heres_thinking(self):
|
||||
"""The belt-and-suspenders guard on titles that slip past the strip."""
|
||||
from api.streaming import _looks_invalid_generated_title
|
||||
assert _looks_invalid_generated_title("Here's a thinking process about Python"), (
|
||||
"_looks_invalid_generated_title must reject 'Here's a thinking ...' titles"
|
||||
)
|
||||
|
||||
def test_looks_invalid_generated_title_accepts_real_titles(self):
|
||||
"""Normal, non-preamble titles must not be rejected."""
|
||||
from api.streaming import _looks_invalid_generated_title
|
||||
assert not _looks_invalid_generated_title("Python import debugging"), (
|
||||
"Real titles must still pass the invalid-title guard"
|
||||
)
|
||||
Reference in New Issue
Block a user