Last active
March 30, 2024 15:37
-
-
Save zerkalica/04b1f40fe35ffa47c58f98b5351b3a7b to your computer and use it in GitHub Desktop.
media-seq.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace $ { | |
export type $gd_kit_media_resource = { id: string, text: string } | |
export class $gd_kit_media_sequence extends $mol_audio_room { | |
tts_id() { | |
return '' as string | null | undefined | |
} | |
@ $mol_mem_key | |
protected audio_data(data: $gd_kit_media_resource) { | |
$mol_wire_solid() | |
const item = this.$.$gd_speech_from_text.make({ | |
tts_id: () => this.tts_id() ?? '', | |
message_id: () => data.id, | |
text: $mol_const(data.text), | |
end: () => this.play_next(), | |
}) | |
// item.preload_async() | |
return item | |
} | |
@ $mol_action | |
play_now(data: $gd_kit_media_resource) { | |
this.stop() | |
this.add(data) | |
} | |
protected sayed = {} as Record<string, { text: string, index: number } | null> | |
@ $mol_action | |
add(next: $gd_kit_media_resource, preload_only = false) { | |
// Фраза с одним и тем же id может дополниться или поменяться по мере стримминга с сервера (gpt). | |
// В случае дополнения, озвучивать нужно только новую часть. | |
// Также, для ускорения озвучки, фраза бьется на предложения (по !?. + пробел или конец строки). | |
// Меньше нельзя, т.к. tts движку нужна полная фраза для корректной интонации. | |
// Индекс озвученного предложения записывается в sayed[id].index. | |
// При следующем вызове берется следующий индекс. | |
// Если фраза вдруг поменялась, то озвучивать нужно сначала. Поэтому в sayed[id].chunk запоминается уже произнесенный кусок | |
// и сравнивается с началом фразы, если он отличается - надо сбросить index и начать произносить с начала. | |
// Что б небыло утечки памяти, все индексы удаляются, если небыло озвучки какое-то время, сек 10 | |
// Если небыло апдейта 10 сек, то надо произнести фразу сначала. | |
const { id, text } = next | |
const key = `${id}${preload_only ? '-preload' : ''}` | |
let current = this.sayed[key] | |
if (current === null) return // игнорируем стриминг, если нажали стоп | |
if (current === undefined) current = this.sayed[key] = { text, index: 0 } | |
// Разбиваем по концам строк, по знакам .!:?; с сохранением разделителей | |
const text_changed = ! next.text.startsWith(current.text) | |
if (text_changed) current.index = 0 | |
current.text = text | |
const chunks = text.split(/(?:(?:(?<=[.!:?;]\s))|(?:(?<=\w+\n)))+/) | |
const chunks_unsayed = chunks.slice(current.index) | |
for (const chunk of chunks_unsayed) { | |
const text = chunk.trim() | |
const chunk_completed = text.match(/[.!:?;\n]+$/) | |
if (! chunk_completed) break | |
current.index++ | |
if (text.length <= 1) continue | |
const item = this.audio_data({ id, text }) | |
this.preloads([ ...this.preloads(), item ]) | |
if (! preload_only) { | |
this.recognitions.push(item) | |
this.current(null) | |
} | |
} | |
this.sended_id_timer?.destructor() | |
this.sended_id_timer = new this.$.$mol_after_timeout(15000, () => { this.sayed = {} }) | |
} | |
protected sended_id_timer = undefined as undefined | $mol_after_timeout | |
protected recognitions = [] as $gd_speech_from_text[] | |
@ $mol_mem | |
protected preloads(next?: readonly $gd_speech_from_text[]) { | |
return next ?? [] | |
} | |
@ $mol_mem | |
current(reset?: null) { return this.recognitions.at(0) ?? null } | |
@ $mol_mem | |
protected waiter() { | |
this.current() | |
return [ $mol_promise<void>() ] | |
} | |
@ $mol_mem | |
wait() { | |
return this.waiter()[0] | |
} | |
@ $mol_action | |
protected play_next() { | |
if (this.recognitions.length === 1) this.waiter()[0]?.done() | |
this.recognitions.shift() | |
this.current(null) | |
this.$.$mol_log3_rise({ | |
place: '$gd_kit_media_sequence.play_next()', | |
message: 'next', | |
count: this.recognitions.length, | |
}) | |
} | |
@ $mol_action | |
stop() { | |
this.current()?.active(false) | |
this.recognitions = [] | |
this.current(null) | |
Object.keys(this.sayed).forEach(key => this.sayed[key] = null) | |
this.$.$mol_log3_rise({ | |
place: '$gd_kit_media_sequence.stop()', | |
message: 'stopped' | |
}) | |
} | |
override destructor(): void { | |
super.destructor() | |
this.stop() | |
} | |
@ $mol_mem | |
override input() { | |
const current = this.current() | |
if (! current) return [] | |
return [ current ] | |
} | |
@ $mol_mem | |
protected active(next?: boolean) { | |
return next ?? true | |
} | |
@ $mol_mem | |
playing() { | |
if (! this.active()) return false | |
this.$.$mol_audio_context.active(true) | |
this.current() | |
try { | |
this.preloads().map(item => item.preload()) | |
this.preloads([]) | |
} catch (e) { | |
$mol_fail_log(e) | |
} | |
if (this.recognitions.length === 0) return false | |
this.output() | |
return true | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment