mirror of
https://github.com/cogwheel0/conduit.git
synced 2026-08-26 11:01:42 +00:00
* fix(voice): keep reasoning blocks out of speech and stop skipping the answer Voice mode speaks the assistant content as it streams, and since the streaming merge started preserving local <details> wrappers, TTS only stripped blocks that were already closed. An open reasoning or tool_calls wrapper was read aloud, and once its </details> landed the block vanished from the split, shifting every later chunk left past the monotonic chunk cursor. The answer itself was then never spoken. TTS now sanitizes through the shared semantic_details helpers and treats an unterminated semantic opener as a hard stop, so the speakable text only ever grows. The streaming feed re-anchors on the text of the last chunk it handed to playback instead of trusting a bare index. * feat(voice): start calls on the speakerphone when nothing is plugged in A voice call runs the audio session in communication mode, so a phone with no accessory attached routes playback to the earpiece. Held like a call it is fine, held like a speakerphone it is barely audible. The coordinator now scans the attached audio devices at call start and engages the loudspeaker only when it finds nothing to play through. A failed scan leaves the route alone, since blaring an answer over someone's headset is worse than a quiet earpiece. It keeps watching for the rest of the call, so pulling headphones out moves playback to the speaker and connecting a headset takes it back off. Pressing the speaker button ends the automatic switching for that call. * fix(voice): serialize audio route and TTS feed changes Route changes are several platform calls deep, so two of them running at once interleaved and the slower one got the last word. A headset pulled out during a reroute could leave the call on the earpiece, and an automatic reroute could land after the user pressed the speaker button or after the call ended. Queue route changes one at a time and re-read the state each one assumed before it applies, so a stale reroute stands down instead of overwriting a newer decision. Deactivation drains the queue before tearing the route down. Streaming TTS feeds had the same shape of problem: a second feed could append its chunks in between the ones an earlier feed was still handing to playback, so sentences could be spoken out of order. Chain the feeds. * fix(voice): catch uppercase semantic details wrappers The complete-block pattern in the TTS sanitizer already ignored case, but the shared opener and block patterns did not, so an uppercase `<DETAILS TYPE="reasoning">` that had not closed yet was read aloud. Tag and attribute names are case-insensitive in HTML and this is model output, so match either case everywhere. * fix(voice): ignore a default route scan that came back too late The accessory scan at the start of a call can still be out when the user presses the speaker button or hangs up. Its result would then resubscribe to device changes and push the call onto the loudspeaker after teardown. Stamp the scan with a call generation and drop the answer if the call it belongs to is over or the user has since chosen a route. * fix(voice): only treat routes a call can actually use as accessories A voice call runs the session in communication mode, which cannot route to A2DP or AirPlay, so a device offering only those is not somewhere the call can play and should not suppress the loudspeaker default. Headsets that also speak HFP still register as bluetoothSco. Accessory detection also required only a matching device type, so a plugged-in microphone counted as somewhere to play. Require an output. Manual speaker toggles are rejected once teardown has started, where honouring them would put communication mode back after the route was handed back. * fix(voice): anchor the speech cursor on spoken text, not one chunk The cursor was re-anchored by searching the fresh split for the text of the last chunk handed to playback. When an answer repeats a sentence, that search can match the wrong copy and skip everything in between, and when the server rewrites the answer the anchor disappears and the cursor stops moving at all. Carry the text already spoken instead and walk the new split against it. The match is positional, so repeated sentences are unambiguous, and a rewrite resumes at the point the two versions stop agreeing rather than replaying or skipping. * fix(tts): strip nested details blocks before speaking A non-greedy `</details>` match stops at the first close tag, so a wrapper nested inside another left the outer block's tail in the text handed to TTS: `<details><details>x</details>secret</details>` spoke `secret`. Replace the pattern with a depth-counting walk shared from `semantic_details.dart`, which also keeps the existing behaviour of withholding the tail of a semantic wrapper that has not closed yet. * fix(voice): re-check the route owner before publishing a reroute The speaker button can be pressed, or the call can end, while the platform calls behind an automatic reroute are still in flight. Publishing afterwards left the speaker control showing a route nobody chose. * fix(tts): keep scanning inside an unclosed ordinary details An ordinary `<details>` still waiting for its close tag kept the whole suffix, so a reasoning wrapper opened inside it reached TTS. Keep the tag and carry on from just past it: nested complete blocks are stripped and a nested open wrapper still truncates. * fix(voice): let a newer device event own the published route A device event that lands while an earlier reroute is mid-flight claims _accessoryAttached before queueing its own work. The earlier reroute then published its now-stale value, so the speaker control showed the old route until the newer operation caught up. * fix(tts): speak on the call route during a voice call Android routes voice-communication and media output separately. Device TTS spoke as USAGE_MEDIA while the call held focus as USAGE_VOICE_COMMUNICATION in MODE_IN_COMMUNICATION, so it ignored the call's speakerphone choice and went silent once the app was backgrounded mid-call. The engine now gets voice-communication audio attributes (and the matching legacy stream param) while a call is up, and goes back to the media stream for read-aloud. * fix(voice): only report a route change the platform took A refused reroute still published to speakerphoneRouteChanges and stuck in _speakerphoneEnabled, so the speaker button pointed at a route nobody was hearing and the enabled == _speakerphoneEnabled guard dropped the next identical device event as already handled. The route calls now answer whether they landed, the flag rolls back when they did not, and neither the device-change path nor the user toggle publishes without a successful move. * fix(tts): hand the engine back to read aloud after a call Disposing the voice-mode provider mid-call left TtsManager in voice-call mode, so a later read-aloud spoke on the call route: earpiece, at call volume. Provider disposal now clears the flag, and so does reset(). reset() also stops rewinding _sessionCounter. A feed queued on the previous chain only checks the active session's id, so reusing an id let it append its old text to the next session, and the id names the server chunk temp dir and background lease that the same stale work tears down. * fix(voice): light the speaker button only on a confirmed route The default route is picked before the audio session exists, so the move to the loudspeaker happens on the next configureFor* pass. The snapshot was set from the pick, not the move, so a refused reroute still lit the speaker button while the call stayed on the earpiece. applyDefaultSpeakerphoneRoute now only sets the preference. The configure pass reports whether the platform took it and announces it on speakerphoneRouteChanges, which is already the one path the snapshot follows. A refused move puts the flag back so the next device event can try again. * fix(voice): queue configure-pass routing behind the other reroutes The listening, speaking and barge-in passes made the same platform route calls as the button and device-change reroutes, off the same _speakerphoneEnabled flag, without going through _routeSerial. A pass that started before a headset was pulled out could finish after the reroute and put the call back on the route it had just left. They now queue with everything else, so each one reads the route decision when it runs rather than when it was scheduled, and the default route is only announced while the loudspeaker is still the current choice. * test(tts): assert with package:checks The rest of the suite asserts with package:checks; this file was the odd one out on expect(), which a review flagged while reading the new voice call tests. * fix(tts): stop a rewrite replaying the sentences it left alone The cursor resumes at the point the old and new splits stop agreeing, so a server that revises one sentence in the middle of an answer queues everything after it a second time. The listener hears the tail twice. Skip forward through the text playback already heard, in order, and only speak the chunks that are not in it. Scanning forward rather than searching the whole string keeps a sentence that genuinely repeats later in the answer spoken once for each time it appears. * fix(tts): hold the session open until the last feed lands finishStreaming marks the response finalized before its own feed reaches the serial chain. If the engine finishes the chunk it is speaking in that window, playback sees a finalized session with nothing left queued, ends it, and the feed carrying the rest of the answer finds no session to append to. The answer stops mid-sentence. Count the feeds still queued or running and treat playback as waiting while any remain. * fix(voice): keep automatic routing after a refused speaker press The speaker button claimed the route before the platform had taken the move, and kept the claim even when the move was refused. Nothing had changed, but the call ignored every accessory event from then on: plug a headset in afterwards and it stayed on the old route. Count presses that are queued or on the wire so an automatic reroute behind one still stands down, and only make the override permanent once the platform reports the move applied. * fix(voice): release the call route when stopping tts throws The engine is shared with read-aloud, and the hand-back sat after the stop call in the same teardown step. A stop that threw skipped it and left read-aloud speaking on the call route. Give it its own step. * fix(voice): hand the route back when the coordinator is disposed first Riverpod gives no order to provider disposal, so the coordinator can go before the controller that would have called deactivate. Its dispose only cancelled the device watch, leaving the phone in communication mode with the call's route still selected. Disposal now runs the same teardown as hanging up, and that teardown is safe to run twice. Session activation also joins the route queue, so a configure pass already in flight cannot reactivate the session after teardown drained the queue, and a default-route scan started during a teardown is rejected instead of riding its generation bump through. * fix(voice): keep the route shut once the coordinator is disposed Teardown lifts the shutter again so the next call can route. A disposed coordinator has no next call, so a deactivate arriving behind disposal left the speaker button able to put the phone back into communication mode after the coordinator was gone. * fix(voice): retry an accessory move the platform refused The transition is claimed in the accessory snapshot before the route operation starts, so the burst of events a single headset sends collapses into one move. A refused move left the claim standing, and since the hardware never moved there is no fresh transition to come: the next notification about the same headset matched the snapshot and was dropped as old news, stranding the call on its previous route. * fix(voice): keep a stale configure pass and overlapping teardowns apart Session configuration is awaited before the activation step joins the route queue, so a teardown that both starts and finishes inside that window puts the shutter back up and the queued step reactivates a call that is over. Each pass now carries the call generation it was started for and stands down when it no longer matches. Overlapping teardowns had the same shape: hanging up and disposal each ran their own finally, and whichever finished first lifted the shutter while the other was still restoring the platform route. The shutter now waits for the last one out. * fix(tts): keep heard sentences the held-back chunk hides Mid stream the trailing chunk stays put until finalization, so a rewrite arriving then leaves the sentences behind it out of the returned spoken text. Finalization has nothing left to match them against and queues them again. Carry the unconsumed heard history along instead. * fix(voice): retry a default route the platform refused A bare phone repeats the same device list rather than announcing a transition, so the snapshot the refused default was picked from makes the next notification look like old news and the call stays on the earpiece. * fix(tts): stop a stale feed holding the next session open A feed already inside the engine's speak call outlives the session that queued it. Its count carried over to the next session, which then waited for a chunk that was never coming and never finished speaking. * fix(voice): drop a speaker press the teardown overtakes A press queued before the call ended still reached the platform behind the teardown, and answered the caller that the route had moved. * fix(tts): hold server completion for a feed still on its way finishStreaming marks the response finalized before its own feed reaches the chain. The device path already waits that window out; the server path ended the session there instead and dropped the rest of the answer. * fix(tts): keep a stale fetch off the next session's bookkeeping A fetch that outlives its session cleared the marker the next session had put down for the same chunk index. * fix(tts): keep a dead session's fetch error to itself A failed fetch from a session that is over was reported as an error against whatever is playing now. |
||
|---|---|---|
| .. | ||
| app | ||
| gradle/wrapper | ||
| .gitignore | ||
| build.gradle.kts | ||
| Gemfile | ||
| Gemfile.lock | ||
| gradle.properties | ||
| settings.gradle.kts | ||