初始提交

This commit is contained in:
fsy
2025-03-18 16:40:47 +08:00
commit 1dfedce690
23 changed files with 4025 additions and 0 deletions

164
src/services/agora.js Normal file
View File

@@ -0,0 +1,164 @@
import AgoraRTC from 'agora-rtc-sdk-ng';
class AgoraService {
constructor() {
this.client = null;
this.localAudioTrack = null;
this.uid = null;
this.isJoined = false;
this.remoteUsers = {};
this.volumeIndicator = null;
this.vadEnabled = true;
this.vadParams = {
interruptDurationMs: 160,
prefixPaddingMs: 300,
silenceDurationMs: 480,
threshold: 0.5
};
}
/**
* Initialize the Agora RTC client
*/
init() {
this.client = AgoraRTC.createClient({ mode: 'rtc', codec: 'vp8' });
this.setupEventListeners();
return this.client;
}
/**
* Set up event listeners for the Agora client
*/
setupEventListeners() {
this.client.on('user-published', async (user, mediaType) => {
await this.client.subscribe(user, mediaType);
if (mediaType === 'audio') {
user.audioTrack.play();
this.remoteUsers[user.uid] = user;
}
});
this.client.on('user-unpublished', (user) => {
if (user.audioTrack) {
user.audioTrack.stop();
}
delete this.remoteUsers[user.uid];
});
this.client.on('user-left', (user) => {
delete this.remoteUsers[user.uid];
});
this.client.on('volume-indicator', (volumes) => {
volumes.forEach((volume) => {
// Handle volume indicator
if (volume.uid === this.uid) {
// Local user's volume
const event = new CustomEvent('local-volume', {
detail: { level: volume.level }
});
window.dispatchEvent(event);
}
});
});
}
/**
* Join a channel with the given token and channel name
* @param {string} token - The token for authentication
* @param {string} channel - The channel name to join
* @param {string} uid - The user ID (optional)
*/
async join(token, channel, uid = null) {
try {
this.uid = uid || `user_${Math.floor(Math.random() * 1000000)}`;
// Join the channel
await this.client.join(token, channel, this.uid);
this.isJoined = true;
// Create and publish local audio track
this.localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack({
AEC: true,
AGC: true,
ANS: true
});
// Enable VAD (Voice Activity Detection)
if (this.vadEnabled && this.localAudioTrack.setVADMode) {
this.localAudioTrack.setVADMode(true, this.vadParams);
}
// Publish local audio track
await this.client.publish([this.localAudioTrack]);
// Enable volume indicator
this.client.enableAudioVolumeIndicator();
return true;
} catch (error) {
console.error('Error joining channel:', error);
return false;
}
}
/**
* Leave the channel and release resources
*/
async leave() {
if (this.localAudioTrack) {
this.localAudioTrack.close();
this.localAudioTrack = null;
}
await this.client.leave();
this.isJoined = false;
this.remoteUsers = {};
}
/**
* Mute or unmute the local audio
* @param {boolean} mute - Whether to mute the audio
*/
muteAudio(mute) {
if (this.localAudioTrack) {
if (mute) {
this.localAudioTrack.setEnabled(false);
} else {
this.localAudioTrack.setEnabled(true);
}
}
}
/**
* Check if the local audio is muted
* @returns {boolean} - Whether the audio is muted
*/
isAudioMuted() {
return this.localAudioTrack ? !this.localAudioTrack.enabled : true;
}
/**
* Set the VAD parameters
* @param {Object} params - The VAD parameters
*/
setVADParams(params) {
this.vadParams = { ...this.vadParams, ...params };
if (this.localAudioTrack && this.localAudioTrack.setVADMode) {
this.localAudioTrack.setVADMode(this.vadEnabled, this.vadParams);
}
}
/**
* Enable or disable VAD
* @param {boolean} enabled - Whether to enable VAD
*/
enableVAD(enabled) {
this.vadEnabled = enabled;
if (this.localAudioTrack && this.localAudioTrack.setVADMode) {
this.localAudioTrack.setVADMode(enabled, this.vadParams);
}
}
}
export default new AgoraService();

161
src/services/api.js Normal file
View File

@@ -0,0 +1,161 @@
import axios from 'axios';
class ApiService {
constructor() {
this.baseUrl = '/api'; // Using Vite proxy instead of direct URL
this.projectId = '01a1debc964a4c6a8df1de2a6ce7aa4d';
this.authToken = 'Basic OGRkM2EzOGUxNTJjNGU1NDlmNWMwOTg0YmRhYzc1ZTE6ZWY1MTI2ZTRmMWFlNGE5MWE0MzVhN2Q0ZDc0YzNlYjg='; // Set the auth token
this.sessionId = null;
this.client = axios.create({
headers: {
'Content-Type': 'application/json',
'Authorization': this.authToken
},
withCredentials: true
});
}
/**
* Set the authentication token
* @param {string} token - The authentication token
*/
setAuthToken(token) {
this.authToken = token;
this.client.defaults.headers.common['Authorization'] = token;
}
/**
* Join a project to start a conversation
* @param {string} channelName - The channel name
* @param {string} agentRtcUid - The agent RTC UID
* @returns {Promise} - The response from the API
*/
async joinProject(channelName = 'convaiconsole_130103', agentRtcUid = '59560') {
try {
const response = await this.client.post(
`${this.baseUrl}/projects/${this.projectId}/join/`,
{
name: channelName,
properties: {
channel: channelName,
agent_rtc_uid: agentRtcUid,
remote_rtc_uids: ["*"],
enable_string_uid: true,
idle_timeout: 120,
llm: {
url: "/ai-api",
api_key: "sk-xVIc9b7EfY7LlPagF31d90F4736f4aE18cB91b5957A40506",
max_history: 10,
system_messages: [
{
role: "system",
content: ""
}
],
params: {
model: "deepseek-r1",
max_token: 1024
},
greeting_message: "你好呀,有什么可以帮您?",
failure_message: "我出错了,请稍等!"
},
asr: {
language: "zh-CN"
},
vad: {
interrupt_duration_ms: 160,
prefix_padding_ms: 300,
silence_duration_ms: 480,
threshold: 0.5
},
tts: {
vendor: "minimax",
params: {
group_id: "wN-fMujjNdcwJ2M3-MbhMHSF6-j_3dT3",
key: "4417529362",
model: "speech-01-turbo-240228",
voice_settings: {
voice_id: "female-shaonv",
speed: 1,
vol: 1,
pitch: 0,
emotion: "neutral"
}
}
},
parameters: {
transcript: {
enable: true,
protocol_version: "v2",
enable_words: false,
redundant: false
},
enable_metrics: true,
audio_scenario: "default"
},
token: "007eJxTYDB8wl8ofHzCsqYtWZqLK64uTOnlWjOtuUS7m6Od7Q7P7+cKDAaGiYYpqUnJlmYmiSbJZokWKWlAvlGiWXKqeWKiScrFJdfTGwIZGV5YrmVhZIBAEF+EITk/rywxE0gW5+ekxhsaGxgaGDMwAADNWiaV",
advanced_features: {
enable_aivad: true
}
}
}
);
this.sessionId = response.data.session_id;
return response.data;
} catch (error) {
console.error('Error joining project:', error);
throw error;
}
}
/**
* Send a message to the AI agent
* @param {string} message - The message to send
* @returns {Promise} - The response from the API
*/
async sendMessage(message) {
if (!this.sessionId) {
throw new Error('No active session. Please join a project first.');
}
try {
const response = await this.client.post(
`${this.baseUrl}/sessions/${this.sessionId}/messages/`,
{
type: 'text',
content: message
}
);
return response.data;
} catch (error) {
console.error('Error sending message:', error);
throw error;
}
}
/**
* End the current session
* @returns {Promise} - The response from the API
*/
async endSession() {
if (!this.sessionId) {
return null;
}
try {
const response = await this.client.delete(
`${this.baseUrl}/sessions/${this.sessionId}/`
);
this.sessionId = null;
return response.data;
} catch (error) {
console.error('Error ending session:', error);
throw error;
}
}
}
export default new ApiService();

185
src/services/audio.js Normal file
View File

@@ -0,0 +1,185 @@
class AudioService {
constructor() {
this.audioContext = null;
this.analyser = null;
this.dataArray = null;
this.bufferLength = 0;
this.audioSource = null;
this.isInitialized = false;
this.audioElement = null;
this.animationFrameId = null;
this.onAudioProcess = null;
}
/**
* Initialize the audio context and analyser
*/
init() {
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
this.audioContext = new AudioContext();
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 256;
this.bufferLength = this.analyser.frequencyBinCount;
this.dataArray = new Uint8Array(this.bufferLength);
this.analyser.connect(this.audioContext.destination);
this.isInitialized = true;
return true;
} catch (error) {
console.error('Error initializing audio service:', error);
return false;
}
}
/**
* Connect a microphone stream to the analyser
* @returns {Promise<boolean>} - Whether the connection was successful
*/
async connectMicrophone() {
if (!this.isInitialized) {
this.init();
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
this.audioSource = this.audioContext.createMediaStreamSource(stream);
this.audioSource.connect(this.analyser);
return true;
} catch (error) {
console.error('Error connecting microphone:', error);
return false;
}
}
/**
* Connect an audio element to the analyser
* @param {HTMLAudioElement} audioElement - The audio element to connect
* @returns {boolean} - Whether the connection was successful
*/
connectAudioElement(audioElement) {
if (!this.isInitialized) {
this.init();
}
try {
this.audioElement = audioElement;
this.audioSource = this.audioContext.createMediaElementSource(audioElement);
this.audioSource.connect(this.analyser);
this.audioSource.connect(this.audioContext.destination);
return true;
} catch (error) {
console.error('Error connecting audio element:', error);
return false;
}
}
/**
* Start analyzing audio data
* @param {Function} callback - The callback function to process audio data
*/
startAnalyzing(callback) {
if (!this.isInitialized) {
return false;
}
this.onAudioProcess = callback;
this.analyzeAudio();
return true;
}
/**
* Analyze audio data and call the callback function
*/
analyzeAudio() {
this.analyser.getByteFrequencyData(this.dataArray);
if (this.onAudioProcess) {
this.onAudioProcess(this.dataArray, this.bufferLength);
}
this.animationFrameId = requestAnimationFrame(() => this.analyzeAudio());
}
/**
* Stop analyzing audio data
*/
stopAnalyzing() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
/**
* Calculate the average volume level from the frequency data
* @returns {number} - The average volume level (0-100)
*/
getAverageVolume() {
if (!this.dataArray) {
return 0;
}
let sum = 0;
for (let i = 0; i < this.bufferLength; i++) {
sum += this.dataArray[i];
}
return sum / this.bufferLength / 255 * 100;
}
/**
* Play audio from a URL
* @param {string} url - The URL of the audio to play
* @returns {Promise} - A promise that resolves when the audio starts playing
*/
playAudio(url) {
return new Promise((resolve, reject) => {
if (!this.audioElement) {
this.audioElement = new Audio();
this.connectAudioElement(this.audioElement);
}
this.audioElement.src = url;
this.audioElement.onplay = resolve;
this.audioElement.onerror = reject;
this.audioElement.play().catch(reject);
});
}
/**
* Stop playing audio
*/
stopAudio() {
if (this.audioElement) {
this.audioElement.pause();
this.audioElement.currentTime = 0;
}
}
/**
* Clean up resources
*/
cleanup() {
this.stopAnalyzing();
this.stopAudio();
if (this.audioSource) {
this.audioSource.disconnect();
this.audioSource = null;
}
if (this.analyser) {
this.analyser.disconnect();
this.analyser = null;
}
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
this.isInitialized = false;
}
}
export default new AudioService();