Skip to content

Instantly share code, notes, and snippets.

@zerkalica
Last active March 30, 2024 15:37
Show Gist options
  • Save zerkalica/04b1f40fe35ffa47c58f98b5351b3a7b to your computer and use it in GitHub Desktop.
Save zerkalica/04b1f40fe35ffa47c58f98b5351b3a7b to your computer and use it in GitHub Desktop.
media-seq.ts
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