feat(mobile): bind device identities and bound voice usage in 0.1.4 dev
This commit is contained in:
1 parent
ac6687181e
commit
5f75dcc99a
24 files changed
+909
-47
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { ASSISTANT_INSTRUCTION, VOICE_TOOLS, VoiceDispatchGate, taskText } from './behavior.mjs'
|
||||
import { VoiceUsage, VOICE_PRICING } from './voice-usage.mjs'
|
||||
|
||||
export function pcm16(samples, sampleRate) {
|
||||
const length = Math.floor(samples.length * 16000 / sampleRate)
|
||||
@@ -12,16 +13,23 @@ export function pcm16(samples, sampleRate) {
|
||||
}
|
||||
|
||||
export class VoiceSession {
|
||||
constructor({ token, model, onText, onDraft, onSubmit, onError, onState }) {
|
||||
Object.assign(this, { token, model, onText, onDraft, onSubmit, onError, onState })
|
||||
constructor({ token, model, onText, onDraft, onSubmit, onError, onState, onUsage, limits, now = () => performance.now() }) {
|
||||
Object.assign(this, { token, model, onText, onDraft, onSubmit, onError, onState, onUsage, now })
|
||||
this.usage = new VoiceUsage(model, limits)
|
||||
this.sources = new Set(); this.stopped = false; this.nextPlayback = 0
|
||||
this.dispatchGate = new VoiceDispatchGate(); this.receiveChain = Promise.resolve()
|
||||
this.audioTranscript = ''; this.preparedDraft = null
|
||||
}
|
||||
async start(microphone = true) {
|
||||
if (this.stopped) return
|
||||
if (this.model !== VOICE_PRICING.model) throw new Error('Bu ses modelinin maliyet tarifesi doğrulanmadı; bağlantı açılmadı.')
|
||||
this.startedAt = this.now(); this.lastUsageAt = this.startedAt
|
||||
this.usageTimer = setInterval(() => this.checkLimits(), 1000)
|
||||
this.checkLimits()
|
||||
try {
|
||||
this.context = new AudioContext()
|
||||
await this.context.resume()
|
||||
if (this.stopped) return
|
||||
if (microphone) {
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true } })
|
||||
if (this.stopped) { this.stream.getTracks().forEach(t => t.stop()); return }
|
||||
@@ -45,11 +53,21 @@ export class VoiceSession {
|
||||
this.socket.onmessage = event => { this.receiveChain = this.receiveChain.then(() => this.receive(event)).catch(() => { this.onError('Ses yanıtı işlenemedi.'); this.stop() }) }
|
||||
this.socket.onerror = () => { this.onError('Google ses bağlantısı kurulamadı. Anahtar, model erişimi ve kotayı kontrol edin.'); this.stop() }
|
||||
this.socket.onclose = () => this.stop()
|
||||
this.limit = setTimeout(() => { this.onText('bilgi', 'Bu görüşmenin 10 dakikalık yerel süre sınırı doldu. Bu bir fatura üst sınırı değildir.'); this.stop() }, 10 * 60000)
|
||||
this.setupTimer = setTimeout(() => { if (!this.ready) { this.onError('Google ses başlangıcı zaman aşımına uğradı.'); this.stop() } }, 20000)
|
||||
} catch { this.onError('Mikrofon veya ses bağlantısı açılamadı. Android mikrofon iznini kontrol edin; yazıyla devam edebilirsiniz.'); this.stop() }
|
||||
}
|
||||
send(frame) { if (this.socket?.readyState === 1) this.socket.send(JSON.stringify(frame)) }
|
||||
checkLimits() {
|
||||
if (this.stopped) return
|
||||
const seconds = Math.floor((this.now() - this.startedAt) / 1000)
|
||||
const usage = this.usage.snapshot()
|
||||
this.onUsage?.({ ...usage, seconds })
|
||||
let reason
|
||||
if (usage.reached) reason = 'Belirlediğiniz tahmini maliyet sınırına ulaşıldı.'
|
||||
else if (seconds >= this.usage.limits.minutes * 60) reason = 'Belirlediğiniz yerel görüşme süresi doldu.'
|
||||
else if (this.now() - this.lastUsageAt >= 120000) reason = 'İki dakikadır Google kullanım bildirimi alınamadı; maliyet izlenemediği için görüşme kapatıldı.'
|
||||
if (reason) { this.onText?.('bilgi', `${reason} Bu kontrol kesin fatura üst sınırı değildir.`); this.stop(reason) }
|
||||
}
|
||||
send(frame) { if (!this.stopped && this.socket?.readyState === 1) this.socket.send(JSON.stringify(frame)) }
|
||||
text(text) {
|
||||
if (!this.ready) throw new Error('Ses asistanı bağlantısı henüz hazır değil.')
|
||||
this.interruptPlayback()
|
||||
@@ -69,6 +87,11 @@ export class VoiceSession {
|
||||
async receive(event) {
|
||||
const data = JSON.parse(event.data instanceof Blob ? await event.data.text() : event.data)
|
||||
if (this.stopped) return
|
||||
if (data.usageMetadata) {
|
||||
try { this.usage.add(data.usageMetadata); this.lastUsageAt = this.now(); this.checkLimits() }
|
||||
catch { this.onError?.('Google kullanım bilgisi hesaplanamadı; maliyet takibi güvenilir olmadığı için ses görüşmesi kapatıldı.'); this.stop(); return }
|
||||
if (this.stopped) return
|
||||
}
|
||||
if (data.setupComplete) { this.ready = true; clearTimeout(this.setupTimer); this.onState('Google asistanı bağlı.'); return }
|
||||
if (data.error) { this.onError('Google ses isteğini reddetti. AI Studio erişim ve kotanızı kontrol edin.'); this.stop(); return }
|
||||
const content = data.serverContent
|
||||
@@ -116,12 +139,12 @@ export class VoiceSession {
|
||||
source.start(start); this.nextPlayback = start + buffer.duration
|
||||
}
|
||||
interruptPlayback() { for (const source of this.sources) { try { source.stop() } catch {} }; this.sources.clear(); this.nextPlayback = 0 }
|
||||
stop() {
|
||||
stop(reason = '') {
|
||||
if (this.stopped) return
|
||||
this.stopped = true; this.ready = false; clearTimeout(this.limit); clearTimeout(this.setupTimer)
|
||||
this.stopped = true; this.ready = false; clearInterval(this.usageTimer); clearTimeout(this.setupTimer)
|
||||
this.dispatchGate.invalidate()
|
||||
this.interruptPlayback(); this.stream?.getTracks().forEach(t => t.stop())
|
||||
this.input?.disconnect(); this.capture?.disconnect(); this.silence?.disconnect()
|
||||
this.socket?.close(); void this.context?.close(); this.onState('Ses görüşmesi kapalı. IDE işi devam edebilir.')
|
||||
this.socket?.close(); void this.context?.close(); this.onState?.(`${reason ? reason + ' ' : ''}Ses görüşmesi kapalı. IDE işi devam edebilir.`)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user