···11-<script setup lang="ts"></script>
11+<script setup lang="ts">
22+// A lot of initial code structure jacked from Julie
33+// https://tangled.org/strings/juli.ee/3m2vdj7abbf22
44+// My implementation focuses on making it "Vueish" and implementing TTS
55+import { ref, onMounted } from "vue"
66+77+const debug = ref(false)
88+const websocketStatus = ref('not connected')
99+const voices = ref([])
1010+const selectedVoice = ref(null)
1111+const voiceText = ref('')
1212+1313+const enableTTS = ref(false)
1414+1515+1616+onMounted(async () => {
1717+ try {
1818+ const response = await fetch('/api/voices')
1919+ if (!response.ok) throw new Error('oops');
2020+ voices.value = await response.json()
2121+ } catch (e) {
2222+ console.log(e)
2323+ }
2424+2525+ console.log(voices.value)
2626+})
2727+2828+async function getAudioClip(chatText: string = "", voice: string = "") {
2929+ const options = {
3030+ text: chatText,
3131+ voice: voice
3232+ }
3333+3434+ const req = new Request('/api', {
3535+ method: "POST",
3636+ body: JSON.stringify(options)
3737+ })
3838+3939+ try {
4040+ const response = await fetch(req)
4141+ if (!response.ok) throw new Error('oops')
4242+ const blob = await response.blob()
4343+ const url = URL.createObjectURL(blob)
4444+ const audio = new Audio(url)
4545+ audio.play()
4646+ } catch (e) {
4747+ console.log(e)
4848+ }
4949+}
5050+5151+// INFO customize these!
5252+const channel = "woovie.net"
5353+const wsEndpoint = "wss://stream.place/api/websocket/"
5454+const ttsDelay = 3000 // When first connecting, we do not want TTS to play for the initial 100 messages. This is my workaround. 3000 is my way overshooting, but should solve it pretty well.
5555+const disableRepeatNames = false
5656+5757+const chatMessages = ref([])
5858+5959+let failures = 0
6060+6161+function startChat() {
6262+ websocketStatus.value = "connecting"
6363+6464+ try {
6565+ const ws = new WebSocket(`${wsEndpoint}${channel}`)
6666+6767+ ws.onopen = () => {
6868+ websocketStatus.value = "connected"
6969+ setTimeout(() => {
7070+ enableTTS.value = true
7171+ console.log("enabled TTS")
7272+ }, ttsDelay)
7373+ }
7474+7575+ ws.onmessage = (event) => {
7676+ try {
7777+ const data = JSON.parse(event.data)
7878+7979+ if (data.$type === "place.stream.chat.defs#messageView") {
8080+ // TODO linter mad, typing and shit. TS is neat but low prio issue
8181+ // TODO if we could somehow know that we are getting the initial messages here, we could reverse the payload to ensure order. or, maybe we can get the time from when the message was sent?
8282+ // TODO check if data.record.text startsWith !voice to allow user customization, store to sqlite db by DID
8383+ chatMessages.value.push({
8484+ handle: data.author.handle,
8585+ message: data.record.text,
8686+ color: data.chatProfile.color
8787+ })
8888+ if (enableTTS.value === true) {
8989+ // TODO impl disableRepeatNames so if your chat is getting one guy'd, it doesn't keep saying "person said" and just says the message. it becomes repetitive.
9090+ // TODO alter handle to remove common social sites (.bsky.social, maybe others if there's any big ones???)
9191+ getAudioClip(`${data.author.handle} said ${data.record.text}`)
9292+ }
9393+ }
9494+ } catch (e) {
9595+ console.error("Ooops! ", e)
9696+ }
9797+ }
9898+9999+ ws.onerror = (error) => {
100100+ console.error("Ooops! ", error)
101101+ }
102102+103103+ ws.onclose = () => {
104104+ console.log("closed")
105105+ connectionRestartExponentialBackoff()
106106+ }
107107+ } catch (e) {
108108+ console.error("Ooops! ", e)
109109+ connectionRestartExponentialBackoff()
110110+ }
111111+}
112112+113113+// TODO i _think_ i'm calling this too often. i already removed it fro mws.onerror, but maybe it also doesn't need to be called in the catch on the websocket try?
114114+function connectionRestartExponentialBackoff() {// TODO review this function, i'm fairly certain this isn't working right, idk why tho
115115+ enableTTS.value = false // We need to disable TTS, otherwise on reconnect we'll get a flood of audio
116116+ websocketStatus.value = "disconnected"
117117+ failures += 1
118118+ console.log(`reconnecting in ${3000 * failures}ms`)
119119+ setTimeout(startChat, 3000 * failures)
120120+}
121121+122122+startChat()
123123+</script>
21243125<template>
44- <h1>You did it!</h1>
55- <p>
66- Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
77- documentation
88- </p>
126126+ <div id="chatContainer">
127127+ <div id="chats" v-for="chat in chatMessages">
128128+ <p class="chatLine">
129129+ <!-- TODO linter madge -->
130130+ <span class="chatHandle" :style="`color: rgb(${chat.color.red}, ${chat.color.green}, ${chat.color.blue})`">{{
131131+ chat.handle }}</span>
132132+ <span class="chatMessage">{{ chat.message }}</span>
133133+ </p>
134134+ </div>
135135+ </div>
136136+ <div id="debugWindow" v-if="!debug">
137137+ <p>
138138+ <select name="model" v-model="selectedVoice">
139139+ <option disabled :value="null">Select a voice</option>
140140+ <option v-for="(model, index) in voices" :key="index" :value="index">{{ index }}</option>
141141+ </select>
142142+ </p>
143143+ <p>
144144+ <input v-model="voiceText" type="text" placeholder="Enter text">
145145+ </p>
146146+ <p>
147147+ <button v-on:click="getAudioClip(voiceText)">send the heckin thing</button>
148148+ </p>
149149+ <p>
150150+ webocket status: {{ websocketStatus }}
151151+ </p>
152152+ </div>
9153</template>
101541111-<style scoped></style>
155155+<style>
156156+root {
157157+ size: 16px;
158158+}
159159+</style>
160160+161161+<style scoped>
162162+#debugWindow {
163163+ position: absolute;
164164+ right: 0;
165165+ bottom: 0;
166166+ background-color: rgba(0, 0, 0, 0.5);
167167+ overflow: auto;
168168+}
169169+170170+#debugWindow p {
171171+ margin: 0 auto;
172172+}
173173+174174+.chatLine {
175175+ margin: 0 auto;
176176+}
177177+</style>
+5
start-piper.sh
···11+#!/bin/bash
22+# This is the default voice i chose, and you MUST provide at least one default voice.
33+# get piper how you feel appropriate, i got it from python package, as this Just Works On My Machine. but i don't know best, you decide.
44+DATA_DIR=${HOME}/projects/piper/
55+python -m piper.http_server -m en_US-libritts_r-medium --data-dir ${DATA_DIR}