mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-19 01:51:15 -05:00
refac
This commit is contained in:
1 parent
323139cdf5
commit
a70e96525d
5 files changed
+58
-11
No files matched your search
@@ -76,9 +76,15 @@ class CallProtocol:
|
||||
|
||||
def observe(self, event):
|
||||
kind = event.get('type')
|
||||
if kind == 'conversation.item.input_audio_transcription.completed':
|
||||
if event.get('transcript', '').strip():
|
||||
if kind in {
|
||||
'conversation.item.input_audio_transcription.completed',
|
||||
'conversation.item.input_audio_transcription.failed',
|
||||
}:
|
||||
if kind.endswith('.completed') and event.get('transcript', '').strip():
|
||||
self.transcripts.add(event['item_id'])
|
||||
else:
|
||||
# Rejected turns must leave the provider's history as well as the chat.
|
||||
return {'type': 'conversation.item.delete', 'item_id': event['item_id']}
|
||||
elif kind == 'response.created':
|
||||
self.responses.add(event['response']['id'])
|
||||
self.response_metadata[event['response']['id']] = event['response'].get('metadata') or {}
|
||||
@@ -452,12 +458,14 @@ async def realtime_call(ws: WebSocket):
|
||||
'audio': {
|
||||
'input': {
|
||||
'format': {'type': 'audio/pcm', 'rate': 24000},
|
||||
'noise_reduction': {'type': 'near_field'},
|
||||
'transcription': {'model': config['audio.realtime.transcription_model']},
|
||||
# Wait for the finalized transcript before requesting a response. This gives
|
||||
# every function call an unambiguous input_item_id, even during barge-in.
|
||||
'turn_detection': {
|
||||
'type': 'server_vad',
|
||||
'interrupt_response': True,
|
||||
# The client pauses playback until speech is confirmed.
|
||||
'interrupt_response': False,
|
||||
'create_response': False,
|
||||
},
|
||||
},
|
||||
@@ -502,7 +510,11 @@ async def realtime_call(ws: WebSocket):
|
||||
if kind == 'error':
|
||||
# Provider error text can include prompts or credentials.
|
||||
raise ValueError('Voice provider rejected a request')
|
||||
protocol.observe(event)
|
||||
command = protocol.observe(event)
|
||||
if command:
|
||||
# Clean up before the client can request another response.
|
||||
async with asyncio.timeout(5):
|
||||
await upstream.send_json(command)
|
||||
if kind.startswith(('response.', 'conversation.item.', 'input_audio_buffer.')):
|
||||
async with asyncio.timeout(5):
|
||||
await ws.send_json(event)
|
||||
|
||||
@@ -505,6 +505,17 @@ async def execute_automation(app, automation: AutomationModel) -> None:
|
||||
'automation_id': automation.id,
|
||||
'background_tasks': {},
|
||||
}
|
||||
# Match the chat UI's memory preference, including inherited defaults.
|
||||
ui_settings = (user.settings.ui or {}) if user.settings else {}
|
||||
memory = ui_settings.get('memory')
|
||||
if memory is None:
|
||||
defaults = await Config.get('ui.default_interface_settings') or {}
|
||||
memory = defaults.get('memory')
|
||||
if memory is None:
|
||||
memory = await Config.get('memories.enable')
|
||||
if memory:
|
||||
form_data.setdefault('features', {})['memory'] = True
|
||||
|
||||
# Call the full chat completion pipeline (same as POST /api/chat/completions).
|
||||
# The handler reference is stored on app.state to avoid circular imports.
|
||||
request = _build_request(app, token=token)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import dayjs, { getDayjsLocale } from '$lib/dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
|
||||
|
||||
@@ -30,9 +34,14 @@
|
||||
let localTime = '';
|
||||
|
||||
$: lastSeen = user?.last_active_at
|
||||
? dayjs(user.last_active_at * 1000)
|
||||
.locale(getDayjsLocale($i18n.language))
|
||||
.from(now)
|
||||
: '';
|
||||
$: lastSeenTimestamp = user?.last_active_at
|
||||
? new Date(user.last_active_at * 1000).toLocaleString($i18n.language, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short'
|
||||
timeStyle: 'long'
|
||||
})
|
||||
: '';
|
||||
|
||||
@@ -166,7 +175,11 @@
|
||||
<span class="sr-only">{$i18n.t('Last seen')}</span>
|
||||
</Tooltip>
|
||||
</dt>
|
||||
<dd>{lastSeen}</dd>
|
||||
<dd>
|
||||
<Tooltip content={lastSeenTimestamp}>
|
||||
<span>{lastSeen}</span>
|
||||
</Tooltip>
|
||||
</dd>
|
||||
</div>
|
||||
{/if}
|
||||
</dl>
|
||||
|
||||
@@ -10,6 +10,7 @@ class RealtimeAudioProcessor extends AudioWorkletProcessor {
|
||||
this.queued = 0;
|
||||
this.received = 0;
|
||||
this.playing = false;
|
||||
this.paused = false;
|
||||
this.rendered = new Map();
|
||||
this.ended = new Set();
|
||||
this.ticks = 0;
|
||||
@@ -22,6 +23,8 @@ class RealtimeAudioProcessor extends AudioWorkletProcessor {
|
||||
if (data.type === 'capture') {
|
||||
this.enabled = data.enabled;
|
||||
this.captureLength = 0;
|
||||
} else if (data.type === 'pause') {
|
||||
this.paused = data.paused;
|
||||
} else if (data.type === 'audio') {
|
||||
// Speech can arrive faster than playback; release each chunk after it plays.
|
||||
this.received += data.samples.length;
|
||||
@@ -68,7 +71,7 @@ class RealtimeAudioProcessor extends AudioWorkletProcessor {
|
||||
if (!this.playing && (this.queued >= 1920 || this.ended.has(this.queue[0]?.response_id))) {
|
||||
this.playing = true;
|
||||
}
|
||||
if (this.playing) {
|
||||
if (this.playing && !this.paused) {
|
||||
let offset = 0;
|
||||
while (offset < output.length && this.queue.length) {
|
||||
const chunk = this.queue[0];
|
||||
|
||||
@@ -350,9 +350,11 @@ export class RealtimeCall {
|
||||
this.syncContext();
|
||||
this.audio?.port.postMessage({ type: 'capture', enabled: !this.muted });
|
||||
} else if (type === 'input_audio_buffer.speech_started') {
|
||||
if (this.muted) return;
|
||||
this.receivingSpeech = event.item_id;
|
||||
this.userSpeaking = !this.muted;
|
||||
this.stopSpeaking();
|
||||
// VAD is provisional: retain the audio so a noise-only turn can resume it.
|
||||
this.audio?.port.postMessage({ type: 'pause', paused: true });
|
||||
} else if (type === 'input_audio_buffer.speech_stopped') {
|
||||
if (this.receivingSpeech === event.item_id) this.userSpeaking = false;
|
||||
} else if (
|
||||
@@ -367,6 +369,8 @@ export class RealtimeCall {
|
||||
if (this.inputs.has(event.item_id)) return;
|
||||
const failed = type === 'conversation.item.input_audio_transcription.failed';
|
||||
const text = failed ? '' : event.transcript?.trim();
|
||||
if (text) this.stopSpeaking();
|
||||
if (!this.receivingSpeech) this.audio?.port.postMessage({ type: 'pause', paused: false });
|
||||
if (!text) {
|
||||
// Empty VAD segments are not requests and must not produce assistant replies.
|
||||
if (failed)
|
||||
@@ -453,8 +457,7 @@ export class RealtimeCall {
|
||||
if (this.animationCalls.has(event.item.call_id)) return;
|
||||
this.animationCalls.add(event.item.call_id);
|
||||
let status = 'unavailable';
|
||||
if (!response || this.interrupted.has(event.response_id) || this.receivingSpeech)
|
||||
status = 'cancelled';
|
||||
if (!response || this.interrupted.has(event.response_id)) status = 'cancelled';
|
||||
else {
|
||||
try {
|
||||
const args = JSON.parse(event.item.arguments);
|
||||
@@ -713,12 +716,17 @@ export class RealtimeCall {
|
||||
if (this.muted) {
|
||||
this.inputLevel = 0;
|
||||
this.userSpeaking = false;
|
||||
this.receivingSpeech = '';
|
||||
this.audio?.port.postMessage({ type: 'pause', paused: false });
|
||||
}
|
||||
this.stream?.getAudioTracks().forEach((track) => {
|
||||
track.enabled = !this.muted;
|
||||
});
|
||||
this.audio?.port.postMessage({ type: 'capture', enabled: this.connected && !this.muted });
|
||||
if (this.muted) this.send({ type: 'input_audio_buffer.clear' });
|
||||
if (this.muted) {
|
||||
this.send({ type: 'input_audio_buffer.clear' });
|
||||
this.flush();
|
||||
}
|
||||
this.options.change();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user