Compare commits
2 Commits
82025a19b2
...
9ad7d16a17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ad7d16a17 | ||
|
|
62d131cf14 |
@@ -25,7 +25,11 @@ export default function App() {
|
||||
const [activeSpeakers, setActiveSpeakers] = useState<ActiveSpeaker[]>([]);
|
||||
const [levels, setLevels] = useState<number[]>(Array.from({ length: 32 }, () => 0.04));
|
||||
const [isListening, setIsListening] = useState(false);
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const audioContextListenRef = useRef<AudioContext | null>(null);
|
||||
const audioContextTransmitRef = useRef<AudioContext | null>(null);
|
||||
const streamRef = useRef<MediaStream | null>(null);
|
||||
const processorRef = useRef<ScriptProcessorNode | null>(null);
|
||||
const userTimelinesRef = useRef(new Map<number, number>());
|
||||
|
||||
const activeTab = uiState.activeTab || "voice";
|
||||
@@ -44,7 +48,7 @@ export default function App() {
|
||||
const average = int16Array.length ? sum / int16Array.length : 0;
|
||||
setLevels((prev) => prev.map((_, index) => Math.max(0.04, average * (0.5 + Math.sin(index * 0.6 + Date.now() / 140) * 0.35 + 0.65) * 5)));
|
||||
|
||||
const audioContext = audioContextRef.current;
|
||||
const audioContext = audioContextListenRef.current;
|
||||
if (!isListening || !audioContext) return;
|
||||
const float32Array = new Float32Array(int16Array.length);
|
||||
for (let i = 0; i < int16Array.length; i++) float32Array[i] = int16Array[i] / 32768;
|
||||
@@ -72,6 +76,73 @@ export default function App() {
|
||||
onPcm: handleIncomingPcm,
|
||||
});
|
||||
|
||||
const stopStreamingLocal = useCallback(() => {
|
||||
setIsStreaming(false);
|
||||
if (processorRef.current) {
|
||||
processorRef.current.disconnect();
|
||||
processorRef.current = null;
|
||||
}
|
||||
if (audioContextTransmitRef.current) {
|
||||
audioContextTransmitRef.current.close();
|
||||
audioContextTransmitRef.current = null;
|
||||
}
|
||||
if (streamRef.current) {
|
||||
for (const track of streamRef.current.getTracks()) track.stop();
|
||||
streamRef.current = null;
|
||||
}
|
||||
setLevels(Array.from({ length: 32 }, () => 0.04));
|
||||
}, []);
|
||||
|
||||
const startStreamingLocal = useCallback(async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
streamRef.current = stream;
|
||||
setIsStreaming(true);
|
||||
|
||||
const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
|
||||
const audioContext = new AudioContextCtor({ sampleRate: SAMPLE_RATE });
|
||||
audioContextTransmitRef.current = audioContext;
|
||||
|
||||
const source = audioContext.createMediaStreamSource(stream);
|
||||
const processor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||
processorRef.current = processor;
|
||||
|
||||
source.connect(processor);
|
||||
processor.connect(audioContext.destination);
|
||||
|
||||
processor.onaudioprocess = (event) => {
|
||||
if (!socket.socketRef.current || socket.socketRef.current.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
const inputData = event.inputBuffer.getChannelData(0);
|
||||
const pcmData = new Int16Array(inputData.length);
|
||||
for (let i = 0; i < inputData.length; i++) {
|
||||
pcmData[i] = Math.max(-1, Math.min(1, inputData[i])) * 32767;
|
||||
}
|
||||
socket.socketRef.current.send(pcmData.buffer);
|
||||
|
||||
// Update local levels from mic
|
||||
let sum = 0;
|
||||
for (let i = 0; i < inputData.length; i++) sum += Math.abs(inputData[i]);
|
||||
const average = inputData.length ? sum / inputData.length : 0;
|
||||
setLevels((prev) => prev.map((_, index) => Math.max(0.04, average * (0.5 + Math.sin(index * 0.6 + Date.now() / 140) * 0.35 + 0.65) * 5)));
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Microphone access failed:", err);
|
||||
setIsStreaming(false);
|
||||
throw err;
|
||||
}
|
||||
}, [socket.socketRef]);
|
||||
|
||||
const toggleStreaming = useCallback(async () => {
|
||||
if (isStreaming) {
|
||||
stopStreamingLocal();
|
||||
await patchUIState({ isStreaming: false });
|
||||
} else {
|
||||
await startStreamingLocal();
|
||||
await patchUIState({ isStreaming: true });
|
||||
}
|
||||
}, [isStreaming, startStreamingLocal, stopStreamingLocal, patchUIState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedVoiceGuild) voice.loadVoiceChannels(selectedVoiceGuild).catch(() => undefined);
|
||||
}, [selectedVoiceGuild, voice.loadVoiceChannels]);
|
||||
@@ -86,15 +157,15 @@ export default function App() {
|
||||
|
||||
const toggleListening = useCallback(async () => {
|
||||
if (isListening) {
|
||||
await audioContextRef.current?.suspend();
|
||||
await audioContextListenRef.current?.suspend();
|
||||
userTimelinesRef.current.clear();
|
||||
setIsListening(false);
|
||||
await patchUIState({ isListening: false });
|
||||
return;
|
||||
}
|
||||
const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
|
||||
audioContextRef.current ??= new AudioContextCtor({ sampleRate: SAMPLE_RATE });
|
||||
await audioContextRef.current.resume();
|
||||
audioContextListenRef.current ??= new AudioContextCtor({ sampleRate: SAMPLE_RATE });
|
||||
await audioContextListenRef.current.resume();
|
||||
setIsListening(true);
|
||||
await patchUIState({ isListening: true });
|
||||
}, [isListening, patchUIState]);
|
||||
@@ -131,11 +202,13 @@ export default function App() {
|
||||
activeSpeakers={activeSpeakers}
|
||||
levels={levels}
|
||||
isListening={isListening}
|
||||
isStreaming={isStreaming}
|
||||
onGuildChange={(guildId) => patchUIState({ selectedVoiceGuild: guildId, selectedVoiceChannel: "" })}
|
||||
onChannelChange={(channelId) => patchUIState({ selectedVoiceChannel: channelId })}
|
||||
onJoin={() => voice.joinVoice(selectedVoiceGuild, selectedVoiceChannel)}
|
||||
onDisconnect={() => voice.leaveVoice()}
|
||||
onListenToggle={toggleListening}
|
||||
onStreamingToggle={toggleStreaming}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="media">
|
||||
|
||||
@@ -15,7 +15,9 @@ interface VoiceControlProps {
|
||||
onJoin: () => void;
|
||||
onDisconnect: () => void;
|
||||
onListenToggle: () => void;
|
||||
onStreamingToggle: () => void;
|
||||
isListening: boolean;
|
||||
isStreaming: boolean;
|
||||
}
|
||||
|
||||
export function VoiceControl({
|
||||
@@ -30,7 +32,9 @@ export function VoiceControl({
|
||||
onJoin,
|
||||
onDisconnect,
|
||||
onListenToggle,
|
||||
onStreamingToggle,
|
||||
isListening,
|
||||
isStreaming,
|
||||
}: VoiceControlProps) {
|
||||
return (
|
||||
<Card>
|
||||
@@ -69,6 +73,9 @@ export function VoiceControl({
|
||||
<Button variant={isListening ? "secondary" : "outline"} onClick={onListenToggle}>
|
||||
{isListening ? "Stop Listening" : "Listen Live"}
|
||||
</Button>
|
||||
<Button variant={isStreaming ? "destructive" : "default"} onClick={onStreamingToggle}>
|
||||
{isStreaming ? "Stop Transmitting" : "Start Transmitting"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -14,11 +14,13 @@ interface VoicePanelProps {
|
||||
activeSpeakers: ActiveSpeaker[];
|
||||
levels: number[];
|
||||
isListening: boolean;
|
||||
isStreaming: boolean;
|
||||
onGuildChange: (guildId: string) => void;
|
||||
onChannelChange: (channelId: string) => void;
|
||||
onJoin: () => void;
|
||||
onDisconnect: () => void;
|
||||
onListenToggle: () => void;
|
||||
onStreamingToggle: () => void;
|
||||
}
|
||||
|
||||
export function VoicePanel(props: VoicePanelProps) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { parentPort } from "node:worker_threads";
|
||||
import { initializeDatabase } from "../database/drizzle";
|
||||
import { buildConversationPromptMessages } from "./conversationContext";
|
||||
import { runModerationAnalysis } from "./llmModerationClient";
|
||||
import {
|
||||
@@ -9,6 +10,8 @@ import type { MessageRecord } from "./types";
|
||||
|
||||
const MAX_CONTEXT_TOKENS = 8000;
|
||||
|
||||
let dbInitialized = false;
|
||||
|
||||
interface AnalysisWorkerRequest {
|
||||
conversationKey: string;
|
||||
messages: MessageRecord[];
|
||||
@@ -32,6 +35,10 @@ async function processAnalysisRequest({
|
||||
messages,
|
||||
}: AnalysisWorkerRequest): Promise<AnalysisWorkerResponse> {
|
||||
try {
|
||||
if (!dbInitialized) {
|
||||
await initializeDatabase();
|
||||
dbInitialized = true;
|
||||
}
|
||||
const firstMessage = messages[0];
|
||||
if (!firstMessage) return { ok: true, conversationKey, rows: [] };
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface SharedUIState {
|
||||
selectedVoiceChannel: string;
|
||||
selectedTextGuild: string;
|
||||
selectedTextChannel: string;
|
||||
activeTab: "voice" | "text";
|
||||
activeTab: "voice" | "messages" | "media" | "review";
|
||||
isListening: boolean;
|
||||
isStreaming: boolean;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ interface SharedUIState {
|
||||
selectedVoiceChannel: string;
|
||||
selectedTextGuild: string;
|
||||
selectedTextChannel: string;
|
||||
activeTab: "voice" | "text";
|
||||
activeTab: "voice" | "messages" | "media" | "review";
|
||||
isListening: boolean;
|
||||
isStreaming: boolean;
|
||||
}
|
||||
@@ -84,7 +84,11 @@ export function normalizeSharedUIState(
|
||||
selectedVoiceChannel: value.selectedVoiceChannel ?? "",
|
||||
selectedTextGuild: value.selectedTextGuild ?? guild,
|
||||
selectedTextChannel: value.selectedTextChannel ?? "",
|
||||
activeTab: value.activeTab === "text" ? "text" : "voice",
|
||||
activeTab: (["voice", "messages", "media", "review"].includes(
|
||||
value.activeTab ?? "",
|
||||
)
|
||||
? value.activeTab
|
||||
: "voice") as "voice" | "messages" | "media" | "review",
|
||||
isListening: value.isListening ?? false,
|
||||
isStreaming: value.isStreaming ?? false,
|
||||
};
|
||||
@@ -117,8 +121,12 @@ function patchSharedUIState(patch: SharedUIStatePatch) {
|
||||
if (typeof patch.selectedTextChannel === "string") {
|
||||
sharedUIState.selectedTextChannel = patch.selectedTextChannel;
|
||||
}
|
||||
if (patch.activeTab === "voice" || patch.activeTab === "text") {
|
||||
sharedUIState.activeTab = patch.activeTab;
|
||||
if (["voice", "messages", "media", "review"].includes(patch.activeTab ?? "")) {
|
||||
sharedUIState.activeTab = patch.activeTab as
|
||||
| "voice"
|
||||
| "messages"
|
||||
| "media"
|
||||
| "review";
|
||||
}
|
||||
if (typeof patch.isListening === "boolean") {
|
||||
sharedUIState.isListening = patch.isListening;
|
||||
@@ -218,6 +226,7 @@ export async function startWebserver(
|
||||
app.use(express.json());
|
||||
|
||||
app.use(express.static(path.join(__dirname, "../public")));
|
||||
app.use(express.static(path.join(__dirname, "../public/app")));
|
||||
|
||||
app.get("/", (_req: Request, res: Response) => {
|
||||
const reactIndex = path.join(__dirname, "../public/app/index.html");
|
||||
|
||||
Reference in New Issue
Block a user