SPONSORED ADVERTISEMENT

SPONSORED AD

Introduction

Building an interactive AI Voice Assistant no longer requires expensive API infrastructure or complex Python backend frameworks like FastAPI or Django. In this tutorial, we will construct a lightweight, high-performance Web-based AI Voice Assistant using PHP (Backend), JavaScript (Frontend), Google Gemini 2.5 Flash-Lite API, and Microsoft Edge-TTS.

This setup supports real-time voice input, live Web Search grounding via Gemini, full audio output playback, dynamic relative time-stamps, and multi-language adaptation (English, Hindi, Urdu, Chinese, Burmese, etc.).

Prerequisites & Server Requirements

Before deploying the application, ensure your Linux VPS or Web Server meets the following specifications:

  1. PHP 7.4+ or 8.x with curl enabled.

  2. Python 3.x and pip installed.

  3. Microsoft Edge-TTS CLI tool installed on your server:

pip install edge-tts

Project File Structure

Organize your application files in your root web server directory as follows:

/voice-assistant/

├── index.html # Frontend Chat Interface & Audio Recorder
├── api.php # Backend API Processor (Gemini & Edge-TTS)
├── audio/ # Directory where generated MP3 files are stored
└── uploads/ # Temporary directory for incoming WebM voice files

1. Backend Script (api.php)

This PHP script handles incoming user audio/text, passes it to the Gemini 2.5 API (with Google Search grounding enabled), and converts the output text into speech using Edge-TTS.

Security Note: Replace your_api_key with your actual Google Gemini API key.

PHP

<?php
// CORS & JSON Header
header(“Access-Control-Allow-Origin: *”);
header(“Access-Control-Allow-Headers: Content-Type”);
header(“Content-Type: application/json; charset=UTF-8″);

// Preflight options request handling
if ($_SERVER[‘REQUEST_METHOD’] === ‘OPTIONS’) {
http_response_code(200);
exit();
}

// CONFIGURATION SETUP
$apiKey = ‘your_api_key’; // Replace with your Gemini API Key

$userMessage = ”;
$audioFilePath = null;
$audioMimeType = null;

// 1. Handle incoming voice recording
if (isset($_FILES[‘audio’]) && $_FILES[‘audio’][‘error’] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . ‘/uploads’;
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$audioFilePath = $uploadDir . ‘/input_’ . time() . ‘.webm’;
move_uploaded_file($_FILES[‘audio’][‘tmp_name’], $audioFilePath);
$audioMimeType = ‘audio/webm’;
}

// 2. Handle incoming text query
if (isset($_POST[‘message’])) {
$userMessage = trim($_POST[‘message’]);
} else {
$rawInput = file_get_contents(‘php://input’);
$inputData = json_decode($rawInput, true);
if (isset($inputData[‘message’])) {
$userMessage = trim($inputData[‘message’]);
}
}

if (empty($userMessage) && !$audioFilePath) {
echo json_encode([‘error’ => ‘No message or audio provided’]);
exit();
}

// STEP 1: Process request with Gemini 2.5 Flash Lite API
$url = “https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent?key=” . $apiKey;

$parts = [];

if ($audioFilePath && file_exists($audioFilePath)) {
$base64Audio = base64_encode(file_get_contents($audioFilePath));
$parts[] = [
“inline_data” => [
“mime_type” => $audioMimeType,
“data” => $base64Audio
]
];
}

$promptText = !empty($userMessage) ? $userMessage : “Please listen to this audio and respond naturally.”;
$parts[] = [“text” => $promptText];

$payload = [
“system_instruction” => [
“parts” => [
[“text” => “You are a friendly AI Voice Assistant. Respond naturally and concisely. Use Google Search for accurate real-time information.”]
]
],
“contents” => [
[
“parts” => $parts
]
],
“tools” => [
[“google_search” => new stdClass()] // Enable Google Search Grounding
]
];

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [‘Content-Type: application/json’]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

$response = curl_exec($ch);
curl_close($ch);

$responseData = json_decode($response, true);

if (isset($responseData[‘error’])) {
echo json_encode([‘reply’ => ‘API Error: ‘ . $responseData[‘error’][‘message’]]);
exit();
}

$replyText = $responseData[‘candidates’][0][‘content’][‘parts’][0][‘text’] ?? ‘Sorry, I could not generate a response.’;

// STEP 2: Generate Audio via Edge-TTS
$audioUrl = null;
$execDebug = [];

if (!empty($replyText)) {
$cleanText = preg_replace(‘/[*#\_`\-]/u’, ‘ ‘, $replyText);

$audioDir = __DIR__ . ‘/audio’;
if (!is_dir($audioDir)) {
mkdir($audioDir, 0777, true);
}

$fileName = ‘voice_’ . time() . ‘_’ . rand(1000, 9999) . ‘.mp3’;
$filePath = $audioDir . ‘/’ . $fileName;

$edgeTtsPath = ‘/usr/local/bin/edge-tts’;

// Change voice parameter here for different languages
$voice = “en-US-AriaNeural”; // Default: English (US)

$command = “{$edgeTtsPath} –voice ” . escapeshellarg($voice) . ” –text ” . escapeshellarg($cleanText) . ” –write-media ” . escapeshellarg($filePath) . ” 2>&1″;

exec($command, $execDebug, $returnStatus);

if (file_exists($filePath)) {
$audioUrl = ‘audio/’ . $fileName;
}
}

// STEP 3: Return JSON response to client
echo json_encode([
‘reply’ => $replyText,
‘audio_url’ => $audioUrl,
‘debug_exec’ => $execDebug
]);
?>

2. Frontend Interface (index.html)

The frontend contains dynamic audio controls, interactive speech recording capabilities, relative timestamps (1m ago), and an audio toggle.

<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>Voice AI Assistant</title>
<style>
* { box-sizing: border-box; }
body { font-family: ‘Segoe UI’, Tahoma, Geneva, Verdana, sans-serif; max-width: 650px; margin: 30px auto; padding: 20px; background-color: #f4f7f6; }

.header-container { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
h2 { margin: 0; color: #333; font-size: 22px; }

.auto-play-toggle { display: flex; align-items: center; gap: 8px; font-size: 14px; color: #555; background: #e9ecef; padding: 6px 12px; border-radius: 20px; cursor: pointer; }
.auto-play-toggle input { cursor: pointer; }

.chat-box { border: 1px solid #ddd; height: 420px; overflow-y: auto; padding: 15px; margin-bottom: 15px; border-radius: 10px; background: #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }

.msg-wrapper { display: flex; flex-direction: column; margin: 12px 0; }
.msg-wrapper.user { align-items: flex-end; }
.msg-wrapper.bot { align-items: flex-start; }

.msg-row { display: flex; align-items: center; gap: 8px; max-width: 85%; }

.msg { padding: 10px 14px; border-radius: 12px; line-height: 1.5; word-wrap: break-word; font-size: 15px; }
.user .msg { background: #007bff; color: white; border-bottom-right-radius: 2px; }
.bot .msg { background: #e9ecef; color: #333; border-bottom-left-radius: 2px; }

.speaker-btn { background: #e0e0e0; border: none; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 14px; transition: 0.2s; }
.speaker-btn:hover { background: #ccc; }

.time-stamp { font-size: 11px; color: #888; margin-top: 4px; padding: 0 4px; }

.input-group { display: flex; gap: 10px; }
input[type=”text”] { flex: 1; padding: 12px; border-radius: 6px; border: 1px solid #ccc; font-size: 16px; }
button { padding: 12px 18px; border-radius: 6px; background: #007bff; color: white; border: none; font-size: 16px; cursor: pointer; transition: 0.2s; }
button:hover { background: #0056b3; }
button:disabled { background: #aaa; cursor: not-allowed; }

.mic-btn { background: #28a745; }
.mic-btn.recording { background: #dc3545; animation: pulse 1s infinite; }
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
</style>
</head>
<body>

<div class=”header-container”>
<h2>AI Voice Assistant</h2>
<label class=”auto-play-toggle”>
<input type=”checkbox” id=”autoPlayToggle” checked>
<span>Auto Voice 🔊</span>
</label>
</div>

<div class=”chat-box” id=”chatBox”></div>

<div class=”input-group”>
<input type=”text” id=”userInput” placeholder=”Ask a question…” onkeydown=”if(event.key===’Enter’) sendMessage()”>
<button id=”micBtn” class=”mic-btn” onclick=”toggleRecording()”>🎙️</button>
<button id=”sendBtn” onclick=”sendMessage()”>Send</button>
</div>

<script>
let currentAudio = null;
let mediaRecorder = null;
let audioChunks = [];
let isRecording = false;

function timeAgo(date) {
const seconds = Math.floor((new Date() – date) / 1000);
if (seconds < 10) return “Just now”;
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
return date.toLocaleTimeString([], { hour: ‘2-digit’, minute: ‘2-digit’ });
}

setInterval(() => {
document.querySelectorAll(‘.time-stamp’).forEach(el => {
const timestamp = el.getAttribute(‘data-time’);
if (timestamp) {
el.innerText = timeAgo(new Date(parseInt(timestamp)));
}
});
}, 10000);

async function sendMessage() {
const inputField = document.getElementById(‘userInput’);
const message = inputField.value.trim();
if (!message) return;

appendMessage(message, ‘user’);
inputField.value = ”;

const formData = new FormData();
formData.append(‘message’, message);

await sendRequest(formData);
}

async function toggleRecording() {
const micBtn = document.getElementById(‘micBtn’);

if (!isRecording) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];

mediaRecorder.ondataavailable = e => audioChunks.push(e.data);
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(audioChunks, { type: ‘audio/webm’ });
appendMessage(‘🎤 Sent Voice Message’, ‘user’);

const formData = new FormData();
formData.append(‘audio’, audioBlob, ‘voice.webm’);

await sendRequest(formData);
};

mediaRecorder.start();
isRecording = true;
micBtn.classList.add(‘recording’);
micBtn.innerText = ‘⏹️ Stop’;
} catch (err) {
alert(“Microphone access denied: ” + err);
}
} else {
mediaRecorder.stop();
isRecording = false;
micBtn.classList.remove(‘recording’);
micBtn.innerText = ‘🎙️’;
}
}

async function sendRequest(formData) {
const sendBtn = document.getElementById(‘sendBtn’);
const micBtn = document.getElementById(‘micBtn’);
const autoPlay = document.getElementById(‘autoPlayToggle’).checked;

sendBtn.disabled = true;
micBtn.disabled = true;

try {
const res = await fetch(‘api.php’, {
method: ‘POST’,
body: formData
});

const data = await res.json();
const reply = data.reply || ‘No response received.’;

appendMessage(reply, ‘bot’, data.audio_url);

if (autoPlay && data.audio_url) {
playAudio(data.audio_url);
}

} catch (err) {
console.error(“Fetch Error:”, err);
appendMessage(‘Server Connection Error.’, ‘bot’);
} finally {
sendBtn.disabled = false;
micBtn.disabled = false;
}
}

function appendMessage(text, type, audioUrl = null) {
const chatBox = document.getElementById(‘chatBox’);
const now = new Date();
const timeString = timeAgo(now);

const wrapper = document.createElement(‘div’);
wrapper.className = `msg-wrapper ${type}`;

const row = document.createElement(‘div’);
row.className = ‘msg-row’;

const msgDiv = document.createElement(‘div’);
msgDiv.className = ‘msg’;
msgDiv.innerText = text;

row.appendChild(msgDiv);

if (type === ‘bot’ && audioUrl) {
const speakerBtn = document.createElement(‘button’);
speakerBtn.className = ‘speaker-btn’;
speakerBtn.innerHTML = ‘🔊’;
speakerBtn.title = ‘Play/Pause Audio’;
speakerBtn.onclick = () => playAudio(audioUrl);
row.appendChild(speakerBtn);
}

const timeDiv = document.createElement(‘div’);
timeDiv.className = ‘time-stamp’;
timeDiv.setAttribute(‘data-time’, now.getTime());
timeDiv.innerText = timeString;

wrapper.appendChild(row);
wrapper.appendChild(timeDiv);

chatBox.appendChild(wrapper);
chatBox.scrollTop = chatBox.scrollHeight;
}

function playAudio(url) {
if (currentAudio && !currentAudio.paused && currentAudio.src.endsWith(url)) {
currentAudio.pause();
return;
}

if (currentAudio) {
currentAudio.pause();
currentAudio.currentTime = 0;
}

currentAudio = new Audio(url);
currentAudio.play().catch(e => {
console.log(“Autoplay blocked or audio error:”, e);
});
}
</script>
</body>
</html>

How to Change Languages (Multi-Language Support)

To switch the spoken AI voice to another language, update the $voice variable in api.php:

Target Language                                                                                                                                                                          Neural Voice Parameter ($voice)

English (US)                                                                                                                                                                       en-US-AriaNeural or en-US-GuyNeural

Hindi (India)                                                                                                                                                                    hi-IN-SwaraNeural or hi-IN-MadhurNeural

Urdu (Pakistan)                                                                                                                                                                  ur-PK-UzmaNeural or ur-PK-AsadNeural

Chinese (Mandarin)                                                                                                                                                      zh-CN-XiaoxiaoNeural or zh-CN-YunxiNeural

Burmese (Myanmar)                                                                                                                                                       my-MM-NilarNeural or my-MM-ThihaNeural