初始提交
This commit is contained in:
164
src/services/agora.js
Normal file
164
src/services/agora.js
Normal 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
161
src/services/api.js
Normal 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
185
src/services/audio.js
Normal 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();
|
||||
Reference in New Issue
Block a user