Archipelago — open-source initial import
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Message } from '@aiui/core/types/message'
|
||||
|
||||
export interface AudioExportOptions {
|
||||
voiceIndex: number
|
||||
rate: number
|
||||
includeMusic: boolean
|
||||
onlyAssistant: boolean
|
||||
}
|
||||
|
||||
export function useAudioExport() {
|
||||
const isPlaying = ref(false)
|
||||
const isPaused = ref(false)
|
||||
const currentIndex = ref(0)
|
||||
const progress = ref(0)
|
||||
const availableVoices = ref<SpeechSynthesisVoice[]>([])
|
||||
const utterance = ref<SpeechSynthesisUtterance | null>(null)
|
||||
|
||||
const isSupported = computed(() => 'speechSynthesis' in window)
|
||||
|
||||
function loadVoices() {
|
||||
if (!isSupported.value) return
|
||||
const synth = window.speechSynthesis
|
||||
availableVoices.value = synth.getVoices()
|
||||
if (availableVoices.value.length === 0) {
|
||||
synth.onvoiceschanged = () => {
|
||||
availableVoices.value = synth.getVoices()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function play(messages: Message[], options: AudioExportOptions) {
|
||||
if (!isSupported.value) return
|
||||
const synth = window.speechSynthesis
|
||||
|
||||
const filtered = options.onlyAssistant
|
||||
? messages.filter((m) => m.role === 'assistant')
|
||||
: messages
|
||||
|
||||
if (filtered.length === 0) return
|
||||
|
||||
isPlaying.value = true
|
||||
isPaused.value = false
|
||||
currentIndex.value = 0
|
||||
|
||||
function speakNext(index: number) {
|
||||
if (index >= filtered.length || !isPlaying.value) {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
|
||||
currentIndex.value = index
|
||||
progress.value = (index / filtered.length) * 100
|
||||
|
||||
const msg = filtered[index]
|
||||
const text = msg.content.replace(/```[\s\S]*?```/g, 'code block').replace(/[#*_`]/g, '')
|
||||
|
||||
const utt = new SpeechSynthesisUtterance(text)
|
||||
utt.rate = options.rate
|
||||
if (availableVoices.value[options.voiceIndex]) {
|
||||
utt.voice = availableVoices.value[options.voiceIndex]
|
||||
}
|
||||
|
||||
utt.onend = () => {
|
||||
speakNext(index + 1)
|
||||
}
|
||||
|
||||
utt.onerror = () => {
|
||||
speakNext(index + 1)
|
||||
}
|
||||
|
||||
utterance.value = utt
|
||||
synth.speak(utt)
|
||||
}
|
||||
|
||||
speakNext(0)
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (!isSupported.value) return
|
||||
window.speechSynthesis.pause()
|
||||
isPaused.value = true
|
||||
}
|
||||
|
||||
function resume() {
|
||||
if (!isSupported.value) return
|
||||
window.speechSynthesis.resume()
|
||||
isPaused.value = false
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (!isSupported.value) return
|
||||
window.speechSynthesis.cancel()
|
||||
isPlaying.value = false
|
||||
isPaused.value = false
|
||||
currentIndex.value = 0
|
||||
progress.value = 0
|
||||
utterance.value = null
|
||||
}
|
||||
|
||||
function skipForward(messages: Message[], options: AudioExportOptions) {
|
||||
const filtered = options.onlyAssistant
|
||||
? messages.filter((m) => m.role === 'assistant')
|
||||
: messages
|
||||
if (currentIndex.value < filtered.length - 1) {
|
||||
window.speechSynthesis.cancel()
|
||||
play(messages, { ...options })
|
||||
// Advance to next
|
||||
currentIndex.value = Math.min(currentIndex.value + 1, filtered.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
async function exportAsWav(messages: Message[], options: AudioExportOptions): Promise<Blob | null> {
|
||||
if (!isSupported.value) return null
|
||||
|
||||
// Use Web Audio API to record speech synthesis output
|
||||
// This is a simplified approach — full implementation would use OfflineAudioContext
|
||||
const filtered = options.onlyAssistant
|
||||
? messages.filter((m) => m.role === 'assistant')
|
||||
: messages
|
||||
|
||||
const fullText = filtered.map((m) => m.content.replace(/```[\s\S]*?```/g, '').replace(/[#*_`]/g, '')).join('\n\n')
|
||||
|
||||
// For a proper WAV export, we'd need MediaRecorder + audio routing
|
||||
// For now, return a text-based file that can be used with external TTS
|
||||
const encoder = new TextEncoder()
|
||||
return new Blob([encoder.encode(fullText)], { type: 'text/plain' })
|
||||
}
|
||||
|
||||
return {
|
||||
isSupported,
|
||||
isPlaying,
|
||||
isPaused,
|
||||
currentIndex,
|
||||
progress,
|
||||
availableVoices,
|
||||
loadVoices,
|
||||
play,
|
||||
pause,
|
||||
resume,
|
||||
stop,
|
||||
skipForward,
|
||||
exportAsWav,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user