diff --git a/bricks/audio.js b/bricks/audio.js index 532cc76..29badaa 100644 --- a/bricks/audio.js +++ b/bricks/audio.js @@ -28,13 +28,16 @@ bricks.AudioPlayer = class extends bricks.JsWidget { if (this.opts.autoplay){ this.audio.addEventListener('canplay', this.play.bind(this)); } + this.audio.addEventListener('ended', this.playended.bind(this)); this.audio.style.width = "100%" this.dom_element.appendChild(this.audio); if ( this.url ){ this.set_source(this.url); } } - + playended(e){ + this.dispatch('ended'); + } set_stream_urls(response){ async function* dyn_urls(response) { const reader = response.body.getReader(); @@ -56,6 +59,7 @@ bricks.AudioPlayer = class extends bricks.JsWidget { } this.url_generator = dyn_urls(response); this.srcList = []; + this.notBegin = true; schedule_once(this.load_queue_url.bind(this), 0.1); } async load_queue_url(){ @@ -93,7 +97,7 @@ bricks.AudioPlayer = class extends bricks.JsWidget { this.audio.appendChild(this.source); } this.url = this.audio.src = url; - bricks.debug(this.audio.src,' new src seted'); + console.log(this.audio.src,' new src seted'); } set_source_from_response(resp){ // 将服务器返回的数据设置为音频源 @@ -113,6 +117,7 @@ bricks.AudioPlayer = class extends bricks.JsWidget { } set_url(url){ this.set_source(url); + this.audio.play(); } } @@ -296,35 +301,58 @@ bricks.AudioRecorder = class extends bricks.HBox { } } -bricks.SttClient = class extends bricks.VBox { - /* - { - "upload_url" - } - */ +bricks.TextedAudioPlayer = class extends bricks.VBox { constructor(opts){ super(opts); - this.recorder = new bricks.AudioRecorder({ - icon_rate:2, - upload_url:opts.upload_url, - upload_resp:'json' + this.audio = new bricks.AudioPlayer({ + height:'60px' }); - this.add_widget(this.recorder); - this.result_text = new bricks.Text({ - text:'', - dynsize:true + this.audio.bind('ended', this.playnext.bind(this)); + var filler, panel; + filler = new bricks.Filler({}); + this.add_widget(this.audio); + this.add_widget(filler); + panel = new bricks.VScrollPanel({ + height: '100%' }); - this.add_widget(this.result_text); - this.recorder.bind('uploaded', this.set_result_text.bind(this)); + filler.add_widget(panel); + this.textw = new bricks.Text({ + text: ' ', + wrap: true + }); + panel.add_widget(this.textw); + this.streaming_buffer = []; + this.wait_play = true; } - set_result_text(event){ - var txt = event.params.text; - this.result_text.set_text(txt); + async playnext(){ + var json = this.streaming_buffer.shift(); + console.log('======', json); + if (json && ! json.done){ + var url = base64_to_url(json.audio); + this.audio.set_url(url); + this.wait_play = false; + this.textw.set_text(json.text); + await this.audio.play(); + } else { + this.textw.set_text(''); + this.audio.set_url(null); + this.wait_play = true; + } + } + async set_stream_urls(response){ + this.wait_play = true; + await streamResponseJson(response, this.load_stream_data.bind(this)); + } + async load_stream_data(json){ + console.log('***', json); + this.streaming_buffer.push(json); + if (this.wait_play) { + await this.playnext(); + } } } - bricks.Factory.register('AudioPlayer', bricks.AudioPlayer); bricks.Factory.register('AudioRecorder', bricks.AudioRecorder); -bricks.Factory.register('SttClient', bricks.SttClient); +bricks.Factory.register('TextedAudioPlayer', bricks.TextedAudioPlayer); diff --git a/bricks/utils.js b/bricks/utils.js index 1b08291..77ea8f4 100644 --- a/bricks/utils.js +++ b/bricks/utils.js @@ -1,6 +1,53 @@ var bricks = window.bricks || {}; bricks.bug = false; +async function streamResponseJson(response, onJson) { + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ""; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + // 按换行切分 NDJSON + let lines = buffer.split("\n"); + buffer = lines.pop(); // 可能是不完整的一行,留到下一轮 + for (const line of lines) { + if (line.trim()) { + try { + const json = JSON.parse(line); + onJson(json); // 👈 回调处理每个 JSON 对象 + } catch (err) { + console.warn("Failed to parse JSON line:", line); + } + } + } + } + // 处理最后残留的一行 + if (buffer.trim()) { + try { + const json = JSON.parse(buffer); + onJson(json); + } catch (err) { + console.warn("Failed to parse trailing JSON line:", buffer); + } + } +} + +function base64_to_url(base64, mimeType = "audio/wav") { + const binary = atob(base64); // 解码 Base64 成 binary 字符串 + const len = binary.length; + const bytes = new Uint8Array(len); + + for (let i = 0; i < len; i++) { + bytes[i] = binary.charCodeAt(i); + } + + const blob = new Blob([bytes], { type: mimeType }); + const url = URL.createObjectURL(blob); + return url; +} + function blobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader();