1 parent
b1a02bdb72
commit
9fb2731848
8 files changed
+246
-40
No files matched your search
@@ -19367,10 +19367,7 @@ def _handle_chat_start(handler, body, diag=None):
|
||||
if "model_provider" in body
|
||||
else getattr(s, "model_provider", None)
|
||||
)
|
||||
<<<<<<< ours
|
||||
_pp_provider, _pp_default, _pp_cfg = _read_profile_model_config(s, requested_provider)
|
||||
=======
|
||||
>>>>>>> theirs
|
||||
explicit_model_pick = bool(body.get("explicit_model_pick"))
|
||||
moa_config = None
|
||||
gateway_chat_enabled = webui_gateway_chat_enabled(get_config())
|
||||
@@ -19383,7 +19380,6 @@ def _handle_chat_start(handler, body, diag=None):
|
||||
moa_config = resolve_moa_config()
|
||||
except RuntimeError as e:
|
||||
return bad(handler, str(e), 503)
|
||||
_pp_provider, _pp_default = _read_profile_model_config(s, requested_provider)
|
||||
diag.stage("resolve_model_provider") if diag else None
|
||||
model, model_provider, normalized_model = _resolve_compatible_session_model_state(
|
||||
requested_model,
|
||||
|
||||
+127
-19
@@ -645,6 +645,11 @@ function _micToastKeyForRecognitionError(error){
|
||||
let _finalText='';
|
||||
let _prefix='';
|
||||
let _isRecording=false;
|
||||
let _micHoldTimer=null;
|
||||
let _micHoldActive=false;
|
||||
let _micPointerDown=false;
|
||||
let _micStartSeq=0;
|
||||
const _micHoldThresholdMs=300;
|
||||
|
||||
function _setButtonTooltipAndKey(btn, key){
|
||||
const text = t(key);
|
||||
@@ -751,14 +756,16 @@ function _micToastKeyForRecognitionError(error){
|
||||
}
|
||||
}
|
||||
|
||||
function _stopTracks(){
|
||||
if(mediaStream){
|
||||
mediaStream.getTracks().forEach(track=>track.stop());
|
||||
mediaStream=null;
|
||||
function _stopTracks(stream=mediaStream){
|
||||
if(stream){
|
||||
stream.getTracks().forEach(track=>track.stop());
|
||||
if(mediaStream===stream) mediaStream=null;
|
||||
}
|
||||
}
|
||||
|
||||
function _stopMic(){
|
||||
_micStartSeq+=1;
|
||||
_isRecording=false;
|
||||
if(!window._micActive) return;
|
||||
// Stop the backend that was ACTIVE WHEN RECORDING STARTED — not whatever
|
||||
// _rawAudioMode says now. The user can toggle Settings → Sound mid-recording,
|
||||
@@ -868,7 +875,31 @@ function _micToastKeyForRecognitionError(error){
|
||||
|
||||
_probeServerSttCapability();
|
||||
|
||||
btn.onclick=async()=>{
|
||||
function _clearMicHoldTimer(){
|
||||
if(_micHoldTimer){
|
||||
clearTimeout(_micHoldTimer);
|
||||
_micHoldTimer=null;
|
||||
}
|
||||
}
|
||||
|
||||
function _resetMicHoldState(){
|
||||
_clearMicHoldTimer();
|
||||
_micHoldActive=false;
|
||||
_micPointerDown=false;
|
||||
}
|
||||
|
||||
function _micButtonAvailable(){
|
||||
if(!btn||btn.disabled) return false;
|
||||
if(btn.style.display==='none') return false;
|
||||
if(btn.classList.contains('composer-control-hidden')) return false;
|
||||
if(btn.getAttribute('aria-hidden')==='true') return false;
|
||||
if(window.getComputedStyle&&window.getComputedStyle(btn).display==='none') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async function _startMicCapture(holdRequired=false){
|
||||
if(!_micButtonAvailable()) return;
|
||||
const startSeq=++_micStartSeq;
|
||||
// Race-condition guard: ignore rapid double-clicks
|
||||
if(_isRecording){
|
||||
_stopMic();
|
||||
@@ -900,26 +931,38 @@ function _micToastKeyForRecognitionError(error){
|
||||
return;
|
||||
}
|
||||
try{
|
||||
mediaStream=await navigator.mediaDevices.getUserMedia({audio:true});
|
||||
const captureStream=await navigator.mediaDevices.getUserMedia({audio:true});
|
||||
if(startSeq!==_micStartSeq||!_micButtonAvailable()||(holdRequired&&!_micHoldActive)){
|
||||
_isRecording=false;
|
||||
_stopTracks(captureStream);
|
||||
return;
|
||||
}
|
||||
mediaStream=captureStream;
|
||||
const preferredTypes=['audio/webm;codecs=opus','audio/webm','audio/ogg;codecs=opus','audio/ogg'];
|
||||
const mimeType=preferredTypes.find(type=>window.MediaRecorder.isTypeSupported?.(type))||'';
|
||||
mediaRecorder=new MediaRecorder(mediaStream,mimeType?{mimeType}:undefined);
|
||||
const captureMode=_rawAudioMode?'media-raw':'media-transcribe';
|
||||
const recorder=new MediaRecorder(captureStream,mimeType?{mimeType}:undefined);
|
||||
audioChunks=[];
|
||||
mediaRecorder.ondataavailable=e=>{if(e.data&&e.data.size)audioChunks.push(e.data);};
|
||||
mediaRecorder.onerror=()=>{
|
||||
const captureChunks=audioChunks;
|
||||
recorder.ondataavailable=e=>{if(e.data&&e.data.size)captureChunks.push(e.data);};
|
||||
recorder.onerror=()=>{
|
||||
const isCurrentCapture=mediaRecorder===recorder||mediaStream===captureStream;
|
||||
_isRecording=false;
|
||||
_setRecording(false);
|
||||
if(mediaRecorder===recorder) mediaRecorder=null;
|
||||
if(isCurrentCapture) _setRecording(false);
|
||||
window._micPendingSend=false;
|
||||
_stopTracks();
|
||||
_stopTracks(captureStream);
|
||||
showToast(t('mic_network'));
|
||||
};
|
||||
mediaRecorder.onstop=async()=>{
|
||||
recorder.onstop=async()=>{
|
||||
const isCurrentCapture=mediaRecorder===recorder||mediaStream===captureStream;
|
||||
if(mediaRecorder===recorder) mediaRecorder=null;
|
||||
_isRecording=false;
|
||||
const blob=new Blob(audioChunks,{type:mediaRecorder.mimeType||mimeType||'audio/webm'});
|
||||
_setRecording(false);
|
||||
_stopTracks();
|
||||
const blob=new Blob(captureChunks,{type:recorder.mimeType||mimeType||'audio/webm'});
|
||||
if(isCurrentCapture) _setRecording(false);
|
||||
_stopTracks(captureStream);
|
||||
if(blob.size){
|
||||
if(_activeCaptureMode==='media-raw'){
|
||||
if(captureMode==='media-raw'){
|
||||
await _sendRawAudio(blob);
|
||||
}else{
|
||||
await _transcribeBlob(blob);
|
||||
@@ -930,16 +973,81 @@ function _micToastKeyForRecognitionError(error){
|
||||
}
|
||||
_applyDeferredServerSttFlip();
|
||||
};
|
||||
_activeCaptureMode=_rawAudioMode?'media-raw':'media-transcribe';
|
||||
mediaRecorder.start();
|
||||
_activeCaptureMode=captureMode;
|
||||
mediaRecorder=recorder;
|
||||
recorder.start();
|
||||
_setRecording(true);
|
||||
}catch(err){
|
||||
if(startSeq!==_micStartSeq) return;
|
||||
_isRecording=false;
|
||||
window._micPendingSend=false;
|
||||
_stopTracks();
|
||||
showToast(t(_micToastKeyForRecognitionError('not-allowed')||'mic_denied'));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function _toggleMicCapture(){
|
||||
if(!_micButtonAvailable()) return;
|
||||
if(window._micActive){
|
||||
_stopMic();
|
||||
return;
|
||||
}
|
||||
await _startMicCapture();
|
||||
}
|
||||
window._toggleMicCapture=_toggleMicCapture;
|
||||
|
||||
btn.addEventListener('pointerdown',e=>{
|
||||
if(e.button!==0) return;
|
||||
if(!_micButtonAvailable()) return;
|
||||
_resetMicHoldState();
|
||||
_micPointerDown=true;
|
||||
_micHoldTimer=setTimeout(async()=>{
|
||||
_micHoldTimer=null;
|
||||
if(!_micPointerDown||window._micActive) return;
|
||||
_micHoldActive=true;
|
||||
await _startMicCapture(true);
|
||||
},_micHoldThresholdMs);
|
||||
});
|
||||
|
||||
btn.addEventListener('pointerup',async e=>{
|
||||
if(e.button!==0||!_micPointerDown) return;
|
||||
_clearMicHoldTimer();
|
||||
if(_micHoldActive){
|
||||
_micHoldActive=false;
|
||||
_micPointerDown=false;
|
||||
_stopMic();
|
||||
return;
|
||||
}
|
||||
_micPointerDown=false;
|
||||
await _toggleMicCapture();
|
||||
});
|
||||
|
||||
btn.addEventListener('pointerleave',()=>{
|
||||
_clearMicHoldTimer();
|
||||
if(_micHoldActive){
|
||||
_micHoldActive=false;
|
||||
_micPointerDown=false;
|
||||
_stopMic();
|
||||
return;
|
||||
}
|
||||
_micPointerDown=false;
|
||||
});
|
||||
|
||||
btn.addEventListener('pointercancel',()=>{
|
||||
_clearMicHoldTimer();
|
||||
if(_micHoldActive){
|
||||
_micHoldActive=false;
|
||||
_micPointerDown=false;
|
||||
_stopMic();
|
||||
return;
|
||||
}
|
||||
_micPointerDown=false;
|
||||
});
|
||||
|
||||
btn.addEventListener('click',async e=>{
|
||||
if(e.detail!==0) return;
|
||||
await _toggleMicCapture();
|
||||
});
|
||||
|
||||
// Wire up the settings checkbox
|
||||
const rawAudioCheckbox = document.getElementById('settingsRawAudio');
|
||||
|
||||
+9
-1
@@ -1644,7 +1644,15 @@ function cmdReasoning(args){
|
||||
}
|
||||
function cmdVoice(){
|
||||
const mic=document.getElementById('btnMic');
|
||||
if(mic&&mic.style.display!=='none'&&!mic.disabled){try{mic.click();return;}catch(_){}}
|
||||
const micVisible=!!(
|
||||
mic
|
||||
&& mic.style.display!=='none'
|
||||
&& !mic.disabled
|
||||
&& !mic.classList.contains('composer-control-hidden')
|
||||
&& mic.getAttribute('aria-hidden')!=='true'
|
||||
&& (!window.getComputedStyle||window.getComputedStyle(mic).display!=='none')
|
||||
);
|
||||
if(micVisible){try{mic.click();return;}catch(_){}}
|
||||
showToast(t('cmd_voice_use_mic'));
|
||||
}
|
||||
|
||||
|
||||
@@ -177,13 +177,15 @@ def test_590_transcribing_status_shown_before_fetch():
|
||||
|
||||
|
||||
def test_590_recording_stops_before_transcribe():
|
||||
"""boot.js: _setRecording(false) must fire in onstop before _transcribeBlob."""
|
||||
onstop_start = BOOT_JS.find("mediaRecorder.onstop")
|
||||
assert onstop_start != -1, "mediaRecorder.onstop not found"
|
||||
onstop_body = BOOT_JS[onstop_start:onstop_start + 400]
|
||||
"""boot.js: _setRecording(false) must fire in onstop before the pinned send path."""
|
||||
onstop_start = BOOT_JS.find("recorder.onstop")
|
||||
assert onstop_start != -1, "recorder.onstop not found"
|
||||
onstop_body = BOOT_JS[onstop_start:onstop_start + 700]
|
||||
rec_pos = onstop_body.find("_setRecording(false)")
|
||||
blob_pos = onstop_body.find("_transcribeBlob(")
|
||||
assert rec_pos != -1 and blob_pos != -1
|
||||
assert rec_pos < blob_pos, (
|
||||
"_setRecording(false) must come before _transcribeBlob so mic icon clears immediately"
|
||||
raw_pos = onstop_body.find("_sendRawAudio(")
|
||||
send_pos = raw_pos if raw_pos != -1 and (blob_pos == -1 or raw_pos < blob_pos) else blob_pos
|
||||
assert rec_pos != -1 and send_pos != -1
|
||||
assert rec_pos < send_pos, (
|
||||
"_setRecording(false) must come before the pinned send path so the mic icon clears immediately"
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _boot_src() -> str:
|
||||
return Path("static/boot.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _commands_src() -> str:
|
||||
return Path("static/commands.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_start_mic_capture_extracted():
|
||||
src = _boot_src()
|
||||
assert re.search(r"async function _startMicCapture\([^)]*\)\{", src)
|
||||
assert "_activeCaptureMode='speech'" in src
|
||||
assert "const captureMode=_rawAudioMode?'media-raw':'media-transcribe';" in src
|
||||
assert "_activeCaptureMode=captureMode;" in src
|
||||
|
||||
|
||||
def test_toggle_mic_capture_exposed_on_window():
|
||||
src = _boot_src()
|
||||
assert "async function _toggleMicCapture()" in src
|
||||
assert "window._toggleMicCapture=_toggleMicCapture;" in src
|
||||
|
||||
|
||||
def test_toggle_helper_respects_hidden_mic_setting():
|
||||
src = _boot_src()
|
||||
assert "function _micButtonAvailable()" in src
|
||||
assert "btn.classList.contains('composer-control-hidden')" in src
|
||||
assert "btn.getAttribute('aria-hidden')==='true'" in src
|
||||
|
||||
|
||||
def test_voice_command_respects_hidden_mic_setting_before_clicking():
|
||||
src = _commands_src()
|
||||
assert "function cmdVoice()" in src
|
||||
assert "mic.classList.contains('composer-control-hidden')" in src
|
||||
assert "mic.getAttribute('aria-hidden')!=='true'" in src
|
||||
assert "showToast(t('cmd_voice_use_mic'));" in src
|
||||
|
||||
|
||||
def test_old_onclick_handler_removed():
|
||||
src = _boot_src()
|
||||
assert "btn.onclick=async()=>{" not in src
|
||||
|
||||
|
||||
def test_pointer_hold_wiring_present():
|
||||
src = _boot_src()
|
||||
assert "let _micHoldTimer=null;" in src
|
||||
assert "let _micHoldActive=false;" in src
|
||||
assert "let _micPointerDown=false;" in src
|
||||
assert "let _micStartSeq=0;" in src
|
||||
assert "const _micHoldThresholdMs=300;" in src
|
||||
assert "btn.addEventListener('pointerdown'" in src
|
||||
assert "btn.addEventListener('pointerup'" in src
|
||||
assert (
|
||||
"btn.addEventListener('pointerleave'" in src
|
||||
or "btn.addEventListener('pointercancel'" in src
|
||||
)
|
||||
assert "_micHoldTimer=setTimeout(async()=>{" in src
|
||||
assert "},_micHoldThresholdMs);" in src
|
||||
|
||||
|
||||
def test_hold_release_routes_to_stop_mic():
|
||||
src = _boot_src()
|
||||
assert re.search(
|
||||
r"btn\.addEventListener\('pointerup',async e=>\{.*?_stopMic\(\);.*?\}\);",
|
||||
src,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert re.search(
|
||||
r"btn\.addEventListener\('pointer(cancel|leave)',\(\)=>\{.*?_stopMic\(\);.*?\}\);",
|
||||
src,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert "const startSeq=++_micStartSeq;" in src
|
||||
assert "if(startSeq!==_micStartSeq||!_micButtonAvailable()||(holdRequired&&!_micHoldActive)){" in src
|
||||
assert "_stopTracks(captureStream);" in src
|
||||
assert "if(startSeq!==_micStartSeq) return;" in src
|
||||
|
||||
|
||||
def test_reserved_ctrl_shift_d_shortcut_is_not_bound():
|
||||
src = _boot_src()
|
||||
assert "(e.metaKey||e.ctrlKey)&&e.shiftKey&&!e.altKey&&(e.key==='d'||e.key==='D')" not in src
|
||||
assert "await window._toggleMicCapture();" not in src
|
||||
|
||||
|
||||
def test_stop_mic_still_exposed_and_active_capture_mode_retained():
|
||||
src = _boot_src()
|
||||
assert "window._stopMic=_stopMic;" in src
|
||||
assert "_activeCaptureMode" in src
|
||||
assert "_stopMic();" in src
|
||||
@@ -70,8 +70,8 @@ def test_permission_style_speech_errors_use_insecure_origin_key_only_on_that_bra
|
||||
def test_dictation_preflights_insecure_origin_before_speech_or_media_capture():
|
||||
body = _slice_between(
|
||||
BOOT_JS,
|
||||
"btn.onclick=async()=>{",
|
||||
"\n // Wire up the settings checkbox",
|
||||
"async function _startMicCapture(holdRequired=false){",
|
||||
"\n\n async function _toggleMicCapture(){",
|
||||
)
|
||||
insecure_idx = body.index("_micOriginNeedsSecureContext()")
|
||||
speech_idx = body.index("recognition.start()")
|
||||
|
||||
@@ -159,10 +159,11 @@ def test_stop_mic_uses_pinned_active_capture_mode_not_current_rawmode():
|
||||
# _stopMic decides backend from the pinned mode, not _rawAudioMode.
|
||||
assert "_activeCaptureMode==='speech'" in _BOOT_JS
|
||||
# onstop dispatches raw-vs-transcribe from the pinned mode too.
|
||||
assert "_activeCaptureMode==='media-raw'" in _BOOT_JS
|
||||
assert "if(captureMode==='media-raw')" in _BOOT_JS
|
||||
# the mode is pinned at both start branches.
|
||||
assert "_activeCaptureMode='speech'" in _BOOT_JS
|
||||
assert "_activeCaptureMode=_rawAudioMode?'media-raw':'media-transcribe'" in _BOOT_JS
|
||||
assert "const captureMode=_rawAudioMode?'media-raw':'media-transcribe';" in _BOOT_JS
|
||||
assert "_activeCaptureMode=captureMode;" in _BOOT_JS
|
||||
|
||||
|
||||
def test_send_raw_audio_honors_explicit_pending_send():
|
||||
|
||||
@@ -425,11 +425,10 @@ def test_boot_js_prefix_variable_declared():
|
||||
def test_boot_js_prefix_captured_on_start():
|
||||
"""_prefix must be set from ta.value when the user starts recording."""
|
||||
js, _ = get_text("/static/boot.js")
|
||||
# _prefix assignment must happen in the btn.onclick else branch (before recognition.start)
|
||||
btn_onclick_idx = js.find("btn.onclick")
|
||||
btn_onclick_end = js.find("};", btn_onclick_idx)
|
||||
onclick_body = js[btn_onclick_idx:btn_onclick_end]
|
||||
assert "_prefix=ta.value" in onclick_body or "_prefix = ta.value" in onclick_body
|
||||
start_idx = js.find("async function _startMicCapture")
|
||||
start_end = js.find("async function _toggleMicCapture", start_idx)
|
||||
start_body = js[start_idx:start_end]
|
||||
assert "_prefix=ta.value" in start_body or "_prefix = ta.value" in start_body
|
||||
|
||||
|
||||
def test_boot_js_onresult_prepends_prefix():
|
||||
|
||||
Reference in new issue
Block a user