mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-11 05:18:44 -08:00
test(asr): [PKG-ASR-05] checkpoint local editor and adjacent core coverage
This commit is contained in:
1 parent
6d7b9292f8
commit
2cede66933
18 files changed
+10908
-240
No files matched your search
@@ -1,247 +1,385 @@
|
||||
var artplayerPluginAsr = (function() {
|
||||
"use strict";
|
||||
const style = ".art-video-player .art-layer-asr {\n pointer-events: none !important;\n position: absolute;\n z-index: 150;\n inset: 0px;\n display: flex;\n flex-direction: column;\n justify-content: end;\n padding: 0 2%;\n font-size: 1rem;\n gap: var(--art-subtitle-gap);\n padding-bottom: var(--art-subtitle-bottom);\n transition: padding-bottom var(--art-transition-duration) ease;\n text-shadow: var(--art-subtitle-border) 1px 0 1px, var(--art-subtitle-border) 0 1px 1px, var(--art-subtitle-border) -1px 0 1px, var(--art-subtitle-border) 0 -1px 1px, var(--art-subtitle-border) 1px 1px 1px, var(--art-subtitle-border) -1px -1px 1px, var(--art-subtitle-border) 1px -1px 1px, var(--art-subtitle-border) -1px 1px 1px;\n}\n.art-video-player.art-control-show .art-layer-asr {\n padding-bottom: calc(var(--art-control-height) + var(--art-subtitle-bottom));\n}\n";
|
||||
function artplayerPluginAsr2(option = {}) {
|
||||
const {
|
||||
length = 3,
|
||||
interval = 100,
|
||||
sampleRate = 16e3,
|
||||
autoHideTimeout = 1e4,
|
||||
onAudioChunk = () => null
|
||||
} = option;
|
||||
return (art) => {
|
||||
let started = false;
|
||||
let audioCtx = null;
|
||||
let sourceNode = null;
|
||||
let recorderNode = null;
|
||||
let gainNode = null;
|
||||
let bufferChunks = [];
|
||||
let timer = null;
|
||||
let workletLoaded = false;
|
||||
let hideTimer = null;
|
||||
let mediaStream = null;
|
||||
let mediaStreamSource = null;
|
||||
const $asr = art.layers.add({
|
||||
name: "asr",
|
||||
html: ""
|
||||
});
|
||||
function splitByPunctuation(text) {
|
||||
return text.split(/(?<=[、。!?!?.])\s*/u).map((s) => s.trim()).filter(Boolean).slice(-length);
|
||||
}
|
||||
function hide() {
|
||||
$asr.style.display = "none";
|
||||
}
|
||||
function append(subtitle) {
|
||||
if (typeof subtitle !== "string")
|
||||
return;
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = setTimeout(hide, autoHideTimeout);
|
||||
$asr.style.display = "";
|
||||
$asr.innerHTML = splitByPunctuation(subtitle).map((line) => `<div class="art-asr-line">${line}</div>`).join("");
|
||||
}
|
||||
const recorderProcessorCode = `
|
||||
class RecorderProcessor extends AudioWorkletProcessor {
|
||||
process(inputs) {
|
||||
const input = inputs[0];
|
||||
if (input && input[0]) {
|
||||
this.port.postMessage(input[0]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
registerProcessor('recorder-processor', RecorderProcessor);
|
||||
`;
|
||||
const createWorkletBlobUrl = () => {
|
||||
const blob = new Blob([recorderProcessorCode], { type: "application/javascript" });
|
||||
return URL.createObjectURL(blob);
|
||||
};
|
||||
function floatTo16BitPCM(float32Array) {
|
||||
const len = float32Array.length;
|
||||
const buffer = new ArrayBuffer(len * 2);
|
||||
const view = new DataView(buffer);
|
||||
for (let i = 0; i < len; i++) {
|
||||
const s = Math.max(-1, Math.min(1, float32Array[i]));
|
||||
view.setInt16(i * 2, s < 0 ? s * 32768 : s * 32767, true);
|
||||
const recorderProcessorCode = `
|
||||
class RecorderProcessor extends AudioWorkletProcessor {
|
||||
process(inputs) {
|
||||
const input = inputs[0];
|
||||
if (input && input[0]) {
|
||||
this.port.postMessage(input[0]);
|
||||
}
|
||||
return buffer;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
registerProcessor('recorder-processor', RecorderProcessor);
|
||||
`;
|
||||
class AudioGraph {
|
||||
constructor(video, sampleRate) {
|
||||
this.video = video;
|
||||
this.sampleRate = sampleRate;
|
||||
this.closed = false;
|
||||
this.direct = false;
|
||||
this.workletLoaded = false;
|
||||
}
|
||||
assertOpen() {
|
||||
if (this.closed)
|
||||
throw new Error("Audio capture was closed");
|
||||
}
|
||||
revokeWorklet() {
|
||||
if (this.blobUrl !== void 0) {
|
||||
URL.revokeObjectURL(this.blobUrl);
|
||||
this.blobUrl = void 0;
|
||||
}
|
||||
function pcmToWav(pcmBuffer, sampleRate2) {
|
||||
const pcmLength = pcmBuffer.byteLength;
|
||||
const wavBuffer = new ArrayBuffer(44 + pcmLength);
|
||||
const view = new DataView(wavBuffer);
|
||||
const writeString = (offset, str) => {
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
view.setUint8(offset + i, str.charCodeAt(i));
|
||||
}
|
||||
};
|
||||
writeString(0, "RIFF");
|
||||
view.setUint32(4, 36 + pcmLength, true);
|
||||
writeString(8, "WAVE");
|
||||
writeString(12, "fmt ");
|
||||
view.setUint32(16, 16, true);
|
||||
view.setUint16(20, 1, true);
|
||||
view.setUint16(22, 1, true);
|
||||
view.setUint32(24, sampleRate2, true);
|
||||
view.setUint32(28, sampleRate2 * 2, true);
|
||||
view.setUint16(32, 2, true);
|
||||
view.setUint16(34, 16, true);
|
||||
writeString(36, "data");
|
||||
view.setUint32(40, pcmLength, true);
|
||||
new Uint8Array(wavBuffer).set(new Uint8Array(pcmBuffer), 44);
|
||||
return wavBuffer;
|
||||
}
|
||||
async function setupAudioContext() {
|
||||
if (audioCtx)
|
||||
return audioCtx;
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate });
|
||||
if (audioCtx.state === "suspended")
|
||||
await audioCtx.resume();
|
||||
return audioCtx;
|
||||
}
|
||||
async function setupAudioSource() {
|
||||
if (!sourceNode && art.video) {
|
||||
try {
|
||||
sourceNode = audioCtx.createMediaElementSource(art.video);
|
||||
return sourceNode;
|
||||
} catch (err) {
|
||||
console.warn("[artplayerPluginAsr] Direct connection failed:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
async initialize() {
|
||||
this.assertOpen();
|
||||
const platform = window;
|
||||
const Context = window.AudioContext || platform.webkitAudioContext;
|
||||
if (!Context)
|
||||
throw new Error("AudioContext is not supported");
|
||||
const context = this.context ?? (this.context = new Context({ sampleRate: this.sampleRate }));
|
||||
if (context.state === "suspended")
|
||||
await context.resume();
|
||||
this.assertOpen();
|
||||
if (!this.workletLoaded) {
|
||||
this.blobUrl = URL.createObjectURL(new Blob([recorderProcessorCode], { type: "application/javascript" }));
|
||||
try {
|
||||
if (!mediaStream) {
|
||||
mediaStream = await captureAudioFromVideo(art.video);
|
||||
}
|
||||
if (mediaStream) {
|
||||
mediaStreamSource = audioCtx.createMediaStreamSource(mediaStream);
|
||||
return mediaStreamSource;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[artplayerPluginAsr] MediaStream fallback failed:", err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function captureAudioFromVideo(videoElement) {
|
||||
try {
|
||||
if (videoElement.captureStream)
|
||||
return videoElement.captureStream();
|
||||
if (videoElement.mozCaptureStream)
|
||||
return videoElement.mozCaptureStream();
|
||||
console.warn("[artplayerPluginAsr] captureStream not supported");
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.warn("[artplayerPluginAsr] Error capturing stream:", err);
|
||||
return null;
|
||||
await context.audioWorklet.addModule(this.blobUrl);
|
||||
this.workletLoaded = true;
|
||||
} finally {
|
||||
this.revokeWorklet();
|
||||
}
|
||||
}
|
||||
async function startCapture() {
|
||||
if (started)
|
||||
return;
|
||||
this.assertOpen();
|
||||
if (!this.source) {
|
||||
try {
|
||||
await setupAudioContext();
|
||||
const audioSource = await setupAudioSource();
|
||||
if (!audioSource)
|
||||
this.source = context.createMediaElementSource(this.video);
|
||||
this.direct = true;
|
||||
} catch (error) {
|
||||
console.warn("[artplayerPluginAsr] Direct connection failed:", error);
|
||||
const capture = this.video.captureStream || this.video.mozCaptureStream;
|
||||
if (!capture)
|
||||
throw new Error("Could not establish audio source");
|
||||
if (!workletLoaded) {
|
||||
const blobUrl = createWorkletBlobUrl();
|
||||
await audioCtx.audioWorklet.addModule(blobUrl);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
workletLoaded = true;
|
||||
}
|
||||
gainNode = audioCtx.createGain();
|
||||
gainNode.gain.value = 1;
|
||||
recorderNode = new AudioWorkletNode(audioCtx, "recorder-processor");
|
||||
recorderNode.port.onmessage = (event) => {
|
||||
bufferChunks.push(new Float32Array(event.data));
|
||||
};
|
||||
audioSource.connect(recorderNode);
|
||||
audioSource.connect(gainNode);
|
||||
gainNode.connect(audioCtx.destination);
|
||||
const CHUNK_SAMPLES = sampleRate * (interval / 1e3);
|
||||
timer = setInterval(async () => {
|
||||
if (bufferChunks.length === 0)
|
||||
return;
|
||||
let accumulated = new Float32Array(0);
|
||||
while (accumulated.length < CHUNK_SAMPLES && bufferChunks.length) {
|
||||
const next = bufferChunks.shift();
|
||||
const tmp = new Float32Array(accumulated.length + next.length);
|
||||
tmp.set(accumulated, 0);
|
||||
tmp.set(next, accumulated.length);
|
||||
accumulated = tmp;
|
||||
}
|
||||
if (accumulated.length < CHUNK_SAMPLES)
|
||||
return;
|
||||
const chunkToSend = accumulated.slice(0, CHUNK_SAMPLES);
|
||||
const pcm = floatTo16BitPCM(chunkToSend);
|
||||
const wav = pcmToWav(pcm, sampleRate);
|
||||
const subtitle = await onAudioChunk({ pcm, wav });
|
||||
append(subtitle);
|
||||
}, interval);
|
||||
started = true;
|
||||
} catch (err) {
|
||||
console.error("[artplayerPluginAsr] Initialization failed:", err);
|
||||
await stopCapture();
|
||||
this.stream = capture.call(this.video);
|
||||
this.source = context.createMediaStreamSource(this.stream);
|
||||
}
|
||||
}
|
||||
async function stopCapture() {
|
||||
if (!started)
|
||||
return;
|
||||
started = false;
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
if (recorderNode) {
|
||||
recorderNode.disconnect();
|
||||
recorderNode.port.onmessage = null;
|
||||
recorderNode = null;
|
||||
}
|
||||
if (gainNode) {
|
||||
gainNode.disconnect();
|
||||
gainNode = null;
|
||||
}
|
||||
bufferChunks = [];
|
||||
}
|
||||
prepare() {
|
||||
this.preparation ?? (this.preparation = this.initialize().catch((error) => {
|
||||
this.preparation = void 0;
|
||||
throw error;
|
||||
}));
|
||||
return this.preparation;
|
||||
}
|
||||
get ownsMediaConnection() {
|
||||
return this.direct;
|
||||
}
|
||||
idle() {
|
||||
const gain = this.gain;
|
||||
this.disconnect();
|
||||
if (this.direct && this.context && this.source) {
|
||||
this.gain = gain || this.context.createGain();
|
||||
if (!gain)
|
||||
this.gain.gain.value = 1;
|
||||
this.source.connect(this.gain);
|
||||
this.gain.connect(this.context.destination);
|
||||
}
|
||||
async function destroy() {
|
||||
await stopCapture();
|
||||
if (mediaStreamSource) {
|
||||
mediaStreamSource.disconnect();
|
||||
mediaStreamSource = null;
|
||||
}
|
||||
if (sourceNode) {
|
||||
sourceNode.disconnect();
|
||||
sourceNode = null;
|
||||
}
|
||||
if (mediaStream) {
|
||||
mediaStream.getTracks().forEach((track) => track.stop());
|
||||
mediaStream = null;
|
||||
}
|
||||
if (audioCtx) {
|
||||
await audioCtx.close();
|
||||
audioCtx = null;
|
||||
}
|
||||
workletLoaded = false;
|
||||
}
|
||||
art.on("video:volumechange", () => {
|
||||
if (gainNode) {
|
||||
gainNode.gain.value = art.volume;
|
||||
}
|
||||
});
|
||||
art.on("play", startCapture);
|
||||
art.on("pause", stopCapture);
|
||||
art.on("destroy", destroy);
|
||||
return {
|
||||
name: "artplayerPluginAsr",
|
||||
stop: destroy,
|
||||
hide,
|
||||
append
|
||||
}
|
||||
connect(receive) {
|
||||
this.assertOpen();
|
||||
const context = this.context;
|
||||
const source = this.source;
|
||||
if (!context || !source)
|
||||
throw new Error("Audio capture is not prepared");
|
||||
const previousGain = this.gain;
|
||||
this.disconnect();
|
||||
const gain = this.gain = previousGain || context.createGain();
|
||||
if (!previousGain)
|
||||
gain.gain.value = this.direct ? 1 : 0;
|
||||
const recorder = this.recorder = new AudioWorkletNode(context, "recorder-processor");
|
||||
recorder.port.onmessage = (event) => {
|
||||
if (!this.closed && this.recorder === recorder)
|
||||
receive(event.data);
|
||||
};
|
||||
source.connect(recorder);
|
||||
if (this.direct)
|
||||
source.connect(gain);
|
||||
else recorder.connect(gain);
|
||||
gain.connect(context.destination);
|
||||
}
|
||||
disconnect() {
|
||||
if (this.recorder) {
|
||||
this.recorder.port.onmessage = null;
|
||||
this.recorder.disconnect();
|
||||
this.recorder = void 0;
|
||||
}
|
||||
this.gain?.disconnect();
|
||||
this.gain = void 0;
|
||||
this.source?.disconnect();
|
||||
}
|
||||
async close() {
|
||||
if (this.closed)
|
||||
return;
|
||||
this.closed = true;
|
||||
this.disconnect();
|
||||
this.revokeWorklet();
|
||||
this.stream?.getTracks().forEach((track) => track.stop());
|
||||
this.stream = void 0;
|
||||
const context = this.context;
|
||||
this.context = void 0;
|
||||
this.source = void 0;
|
||||
if (context && context.state !== "closed")
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
function encodeAudio(samples, sampleRate) {
|
||||
const pcm = new ArrayBuffer(samples.length * 2);
|
||||
const pcmView = new DataView(pcm);
|
||||
for (let index = 0; index < samples.length; index++) {
|
||||
const value = Math.max(-1, Math.min(1, samples[index]));
|
||||
pcmView.setInt16(index * 2, value < 0 ? value * 32768 : value * 32767, true);
|
||||
}
|
||||
const wav = new ArrayBuffer(44 + pcm.byteLength);
|
||||
const header = new DataView(wav);
|
||||
function writeString(offset, value) {
|
||||
for (let index = 0; index < value.length; index++)
|
||||
header.setUint8(offset + index, value.charCodeAt(index));
|
||||
}
|
||||
writeString(0, "RIFF");
|
||||
header.setUint32(4, 36 + pcm.byteLength, true);
|
||||
writeString(8, "WAVE");
|
||||
writeString(12, "fmt ");
|
||||
header.setUint32(16, 16, true);
|
||||
header.setUint16(20, 1, true);
|
||||
header.setUint16(22, 1, true);
|
||||
header.setUint32(24, sampleRate, true);
|
||||
header.setUint32(28, sampleRate * 2, true);
|
||||
header.setUint16(32, 2, true);
|
||||
header.setUint16(34, 16, true);
|
||||
writeString(36, "data");
|
||||
header.setUint32(40, pcm.byteLength, true);
|
||||
new Uint8Array(wav).set(new Uint8Array(pcm), 44);
|
||||
return { pcm, wav };
|
||||
}
|
||||
class SampleQueue {
|
||||
constructor() {
|
||||
this.offset = 0;
|
||||
this.size = 0;
|
||||
}
|
||||
get length() {
|
||||
return this.size;
|
||||
}
|
||||
push(input) {
|
||||
if (input.length === 0)
|
||||
return;
|
||||
const block = { samples: new Float32Array(input) };
|
||||
if (this.tail)
|
||||
this.tail.next = block;
|
||||
else
|
||||
this.head = block;
|
||||
this.tail = block;
|
||||
this.size += input.length;
|
||||
}
|
||||
take(count) {
|
||||
if (!Number.isSafeInteger(count) || count <= 0)
|
||||
throw new RangeError("Sample count must be a positive safe integer");
|
||||
if (count > this.size)
|
||||
return null;
|
||||
const output = new Float32Array(count);
|
||||
let written = 0;
|
||||
while (this.head && written < count) {
|
||||
const samples = this.head.samples;
|
||||
const amount = Math.min(samples.length - this.offset, count - written);
|
||||
output.set(samples.subarray(this.offset, this.offset + amount), written);
|
||||
written += amount;
|
||||
this.offset += amount;
|
||||
if (this.offset === samples.length) {
|
||||
this.head = this.head.next;
|
||||
this.offset = 0;
|
||||
}
|
||||
}
|
||||
if (!this.head)
|
||||
this.tail = void 0;
|
||||
this.size -= count;
|
||||
return output;
|
||||
}
|
||||
clear() {
|
||||
this.head = void 0;
|
||||
this.tail = void 0;
|
||||
this.offset = 0;
|
||||
this.size = 0;
|
||||
}
|
||||
}
|
||||
class Capture {
|
||||
constructor(video, options, append) {
|
||||
this.video = video;
|
||||
this.options = options;
|
||||
this.append = append;
|
||||
this.queue = new SampleQueue();
|
||||
this.closing = Promise.resolve();
|
||||
this.running = false;
|
||||
this.terminal = false;
|
||||
this.epoch = 0;
|
||||
}
|
||||
reset() {
|
||||
this.epoch++;
|
||||
this.pending = void 0;
|
||||
this.queue.clear();
|
||||
}
|
||||
pause() {
|
||||
this.reset();
|
||||
this.running = false;
|
||||
clearInterval(this.timer);
|
||||
this.timer = void 0;
|
||||
this.starting = void 0;
|
||||
this.graph?.idle();
|
||||
}
|
||||
restart() {
|
||||
const active = this.running || Boolean(this.starting);
|
||||
this.pause();
|
||||
if (this.graph && !this.graph.ownsMediaConnection)
|
||||
return this.stop().then(() => active ? this.start() : void 0);
|
||||
return active ? this.start() : Promise.resolve();
|
||||
}
|
||||
start() {
|
||||
if (this.terminal || this.running)
|
||||
return Promise.resolve();
|
||||
if (this.starting)
|
||||
return this.starting;
|
||||
const epoch = this.epoch;
|
||||
const current = () => !this.terminal && this.epoch === epoch;
|
||||
const operation = (async () => {
|
||||
await this.closing;
|
||||
if (!current())
|
||||
return;
|
||||
const { sampleRate, interval } = this.options;
|
||||
const count = Math.floor(sampleRate * interval / 1e3);
|
||||
if (!Number.isSafeInteger(count) || count < 1 || !Number.isFinite(interval) || interval <= 0)
|
||||
throw new Error("Audio chunk length must be positive and finite");
|
||||
const graph = this.graph ?? (this.graph = new AudioGraph(this.video, sampleRate));
|
||||
await graph.prepare();
|
||||
if (!current()) {
|
||||
if (!this.running && this.graph === graph)
|
||||
graph.idle();
|
||||
return;
|
||||
}
|
||||
graph.connect((samples) => {
|
||||
if (!this.running || this.graph !== graph)
|
||||
return;
|
||||
if (this.queue.length + samples.length > Math.max(sampleRate * 60, count * 2)) {
|
||||
console.error("[artplayerPluginAsr] Audio callback backlog exceeded its capture limit");
|
||||
this.pause();
|
||||
return;
|
||||
}
|
||||
this.queue.push(samples);
|
||||
});
|
||||
this.running = true;
|
||||
this.timer = setInterval(() => this.tick(count), interval);
|
||||
})().catch(async (error) => {
|
||||
if (current()) {
|
||||
console.error("[artplayerPluginAsr] Initialization failed:", error);
|
||||
await this.stop();
|
||||
}
|
||||
}).finally(() => {
|
||||
if (this.starting === operation)
|
||||
this.starting = void 0;
|
||||
});
|
||||
this.starting = operation;
|
||||
return operation;
|
||||
}
|
||||
async tick(count) {
|
||||
if (!this.running || this.pending)
|
||||
return;
|
||||
const samples = this.queue.take(count);
|
||||
if (!samples)
|
||||
return;
|
||||
const epoch = this.epoch;
|
||||
const pending = this.pending = {};
|
||||
try {
|
||||
const text = await this.options.onAudioChunk(encodeAudio(samples, this.options.sampleRate));
|
||||
if (this.running && !this.terminal && this.epoch === epoch)
|
||||
this.append(text);
|
||||
} catch (error) {
|
||||
if (this.epoch === epoch && !this.terminal)
|
||||
console.error("[artplayerPluginAsr] Audio callback failed:", error);
|
||||
} finally {
|
||||
if (this.pending === pending)
|
||||
this.pending = void 0;
|
||||
}
|
||||
}
|
||||
stop(terminal = false) {
|
||||
this.terminal || (this.terminal = terminal);
|
||||
this.pause();
|
||||
const graph = this.graph;
|
||||
if (terminal || !graph?.ownsMediaConnection) {
|
||||
this.graph = void 0;
|
||||
this.closing = Promise.all([this.closing, graph?.close()]).then(() => void 0);
|
||||
}
|
||||
return this.closing;
|
||||
}
|
||||
}
|
||||
const style = ".art-video-player .art-layer-asr {\n pointer-events: none !important;\n position: absolute;\n z-index: 150;\n inset: 0px;\n display: flex;\n flex-direction: column;\n justify-content: end;\n padding: 0 2%;\n font-size: 1rem;\n gap: var(--art-subtitle-gap);\n padding-bottom: var(--art-subtitle-bottom);\n transition: padding-bottom var(--art-transition-duration) ease;\n text-shadow: var(--art-subtitle-border) 1px 0 1px, var(--art-subtitle-border) 0 1px 1px, var(--art-subtitle-border) -1px 0 1px, var(--art-subtitle-border) 0 -1px 1px, var(--art-subtitle-border) 1px 1px 1px, var(--art-subtitle-border) -1px -1px 1px, var(--art-subtitle-border) 1px -1px 1px, var(--art-subtitle-border) -1px 1px 1px;\n}\n.art-video-player.art-control-show .art-layer-asr {\n padding-bottom: calc(var(--art-control-height) + var(--art-subtitle-bottom));\n}\n";
|
||||
function createSubtitles(layer, length, autoHideTimeout) {
|
||||
let timer;
|
||||
let destroyed = false;
|
||||
function cancelTimer() {
|
||||
clearTimeout(timer);
|
||||
timer = void 0;
|
||||
}
|
||||
function hide() {
|
||||
if (!destroyed)
|
||||
layer.style.display = "none";
|
||||
}
|
||||
function append(subtitle) {
|
||||
if (destroyed || typeof subtitle !== "string")
|
||||
return;
|
||||
cancelTimer();
|
||||
timer = setTimeout(hide, autoHideTimeout);
|
||||
layer.style.display = "";
|
||||
layer.innerHTML = subtitle.split(/(?<=[、。!?!?.])\s*/u).map((line) => line.trim()).filter(Boolean).slice(-length).map((line) => `<div class="art-asr-line">${line}</div>`).join("");
|
||||
}
|
||||
return {
|
||||
append,
|
||||
hide,
|
||||
destroy() {
|
||||
destroyed = true;
|
||||
cancelTimer();
|
||||
}
|
||||
};
|
||||
}
|
||||
if (typeof document !== "undefined") {
|
||||
if (!document.getElementById("artplayer-plugin-asr")) {
|
||||
const $style = document.createElement("style");
|
||||
$style.id = "artplayer-plugin-asr";
|
||||
$style.textContent = style;
|
||||
document.head.appendChild($style);
|
||||
}
|
||||
function artplayerPluginAsr2(option = {}) {
|
||||
const { length = 3, interval = 100, sampleRate = 16e3, autoHideTimeout = 1e4, onAudioChunk = () => null } = option;
|
||||
return (art) => {
|
||||
const layer = art.layers.add({ name: "asr", html: "" });
|
||||
if (!layer)
|
||||
throw new Error("Could not create ASR subtitle layer");
|
||||
const subtitles = createSubtitles(layer, length, autoHideTimeout);
|
||||
const capture = new Capture(art.video, { interval, sampleRate, onAudioChunk }, subtitles.append);
|
||||
const play = () => capture.start();
|
||||
const pause = () => capture.pause();
|
||||
const restart = () => capture.restart();
|
||||
const stop = () => capture.stop();
|
||||
const destroy = () => {
|
||||
subtitles.destroy();
|
||||
art.off("play", play);
|
||||
art.off("pause", pause);
|
||||
art.off("restart", restart);
|
||||
art.off("destroy", destroy);
|
||||
return capture.stop(true);
|
||||
};
|
||||
art.on("play", play);
|
||||
art.on("pause", pause);
|
||||
art.on("restart", restart);
|
||||
art.on("destroy", destroy);
|
||||
return { name: "artplayerPluginAsr", stop, hide: subtitles.hide, append: subtitles.append };
|
||||
};
|
||||
}
|
||||
Object.defineProperty(artplayerPluginAsr2, "default", { value: artplayerPluginAsr2 });
|
||||
if (typeof document !== "undefined" && !document.getElementById("artplayer-plugin-asr")) {
|
||||
const styleElement = document.createElement("style");
|
||||
styleElement.id = "artplayer-plugin-asr";
|
||||
styleElement.textContent = style;
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
return artplayerPluginAsr2;
|
||||
})();
|
||||
Reference in new issue
Block a user