Alex Austin commited on
Commit
9f4ec4d
·
1 Parent(s): 4995991

vox beta00

Browse files
Files changed (2) hide show
  1. app.py +127 -2
  2. templates/u_dash.html +1982 -0
app.py CHANGED
@@ -1,9 +1,134 @@
1
-
2
  from flask import Flask, request, jsonify, render_template
 
 
 
 
 
 
 
3
 
4
  app = Flask(__name__)
 
 
 
 
 
 
 
 
 
5
 
6
  @app.route("/")
7
  def greet_html():
8
  return render_template("home.html")
9
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from flask import Flask, request, jsonify, render_template
2
+ from datetime import datetime, timedelta
3
+ from flask_cors import CORS
4
+ from TTS.api import TTS
5
+ import os
6
+ import base64
7
+ from helper import save_audio, generate_random_filename, save_to_dataset_repo, video_to_audio, validate_audio_file, ensure_wav_format
8
+ import wave
9
 
10
  app = Flask(__name__)
11
+ CORS(app)
12
+ os.environ["COQUI_TOS_AGREED"] = "1"
13
+
14
+ device = "cpu"
15
+
16
+ tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(device)
17
+
18
+ active_tasks = {}
19
+
20
 
21
  @app.route("/")
22
  def greet_html():
23
  return render_template("home.html")
24
+
25
+ @app.route("/generate_voice", methods=['POST'])
26
+ def generate_voice():
27
+ try:
28
+ data = request.get_json()
29
+ if not data:
30
+ return jsonify({'error': 'No JSON body'}), 400
31
+ video = data.get('video')
32
+ text = data.get('text')
33
+ audio_base64 = data.get('audio')
34
+ task_id = data.get('task_id')
35
+ user_id = data.get('user_id')
36
+ if not user_id:
37
+ return jsonify({'error': 'you are required to signin before you could usw this Ai'})
38
+ if not text:
39
+ return jsonify({'error': 'please input a prompt'})
40
+ if task_id in active_tasks:
41
+ return jsonify({'error': f'There is already an active tasks for {task_id}'}), 409
42
+ active_tasks[task_id]={
43
+ "user_id": user_id,
44
+ "status": "Processing",
45
+ "created_at": datetime.now(),
46
+ }
47
+ process_vox(user_id, text, video, audio_base64, task_id)
48
+ return jsonify({'message': 'Processing started', 'task_id': task_id}), 202
49
+ except Exception as e:
50
+ return jsonify({'error': e}), 500
51
+
52
+ def process_vox(user_id, text, video, audio_base64, task_id):
53
+ try:
54
+ if audio_base64:
55
+ if audio_base64.startswith('data:audio/'):
56
+ audio_base64=audio_base64.split(',', 1)[1]
57
+ temp_audio_path = f'/tmp/temp_ref_{task_id}.wav'
58
+ with open(temp_audio_path, 'wb') as f:
59
+ f.write(base64.b64decode(audio_base64))
60
+ elif video:
61
+ temp_audio_path = video_to_audio(video, output_path=temp_audio_path)
62
+ temp_audio_path = ensure_wav_format(temp_audio_path)
63
+
64
+ valid, msg = validate_audio_file(temp_audio_path, MAX_AUDIO_SIZE_MB)
65
+ if not valid:
66
+ raise Exception(f"Invalid audio file: {msg}")
67
+ result_file= clone(text, temp_audio_path)
68
+ out_dir = "user_audios"
69
+
70
+ os.makedirs(out_dir, exist_ok=True)
71
+ file_name = generate_random_filename("mp3")
72
+ file_path = os.path.join(out_dir, file_name)
73
+
74
+ with open(result_file, 'rb') as src, open(file_path, 'wb') as dst:
75
+ dst.write(src.read())
76
+
77
+ with wave.open(file_path, 'rb') as wf:
78
+ dura = wf.getnframes() / float(wf.getframerate())
79
+ duration=f"{dura:.2f}"
80
+ title=text[:20]
81
+ # Upload + save metadata
82
+ audio_url = save_to_dataset_repo(file_path, f"user/data/audios/{file_name}", file_name)
83
+ active_tasks[task_id].update({'status': 'completed', 'audio_url': audio_url, 'completion_time': datetime.now()})
84
+ save_audio(user_id, audio_url, title or "Audio", text, duration)
85
+ except Exception as e:
86
+ active_tasks[task_id] = {'status': 'failed', 'error': str(e), 'completion_time': datetime.now()}
87
+ finally:
88
+ if os.path.exists(temp_audio_path):
89
+ os.remove(temp_audio_path)
90
+
91
+ def clone(text, audio):
92
+ tts.tts_to_file(text=text, speaker_wav=audio, language="en", file_path="./output.wav")
93
+ return "./output.wav"
94
+
95
+ @app.route('/task_status')
96
+ def task_status():
97
+ task_id = request.args.get("task_id")
98
+ if not task_id:
99
+ return jsonify({'error': 'task_id parameter is required'}), 400
100
+
101
+ if task_id in active_tasks:
102
+ task = active_tasks[task_id]
103
+ response_data = {
104
+ 'status': task['status'],
105
+ 'start_time': task['start_time'].isoformat()
106
+ }
107
+
108
+ if task['status'] == 'complete':
109
+ audio_url = task.get('audio_url')
110
+ completion_time = task.get('completion_time')
111
+ duration = task.get('duration')
112
+ response_data['audio_url'] = audio_url
113
+ response_data['completion_time'] = completion_time.isoformat() if completion_time else None
114
+ response_data['duration'] = duration
115
+
116
+ # Keep completed tasks for a while for status checking
117
+ if completion_time and (datetime.now() - completion_time).total_seconds() > 300:
118
+ # 5 minutes
119
+ del active_tasks[task_id]
120
+
121
+ elif task['status'] == 'failed':
122
+ error_message = task.get('error')
123
+ completion_time = task.get('completion_time')
124
+ response_data['error'] = error_message
125
+ response_data['completion_time'] = completion_time.isoformat() if completion_time else None
126
+
127
+ # Keep failed tasks shorter
128
+ if completion_time and (datetime.now() - completion_time).total_seconds() > 300:
129
+ # 5 minutes
130
+ del active_tasks[task_id]
131
+
132
+ return jsonify(response_data)
133
+ else:
134
+ return jsonify({'status': 'not found'}), 404
templates/u_dash.html ADDED
@@ -0,0 +1,1982 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>VoxAI Pro | Advanced AI Voice Cloning</title>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ :root {
11
+ --primary: #4361ee;
12
+ --primary-dark: #3a56d4;
13
+ --primary-light: #4895ef;
14
+ --secondary: #3a0ca3;
15
+ --accent: #7209b7;
16
+ --accent-light: #9d4edd;
17
+ --success: #4cc9f0;
18
+ --warning: #f72585;
19
+ --light: #f8f9fa;
20
+ --dark: #212529;
21
+ --gray: #6c757d;
22
+ --gray-light: #adb5bd;
23
+ --gradient: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%);
24
+ --gradient-accent: linear-gradient(135deg, #7209b7 0%, #f72585 100%);
25
+ --gradient-light: linear-gradient(135deg, #4895ef 0%, #4cc9f0 100%);
26
+ --card-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
27
+ --card-shadow-hover: 0 20px 40px rgba(0, 0, 0, 0.15);
28
+ --transition: all 0.3s ease;
29
+ }
30
+
31
+ * {
32
+ margin: 0;
33
+ padding: 0;
34
+ box-sizing: border-box;
35
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
36
+ }
37
+
38
+ body {
39
+ background: linear-gradient(135deg, #f5f7fa 0%, #e3e8f5 100%);
40
+ color: var(--dark);
41
+ line-height: 1.6;
42
+ min-height: 100vh;
43
+ padding: 20px;
44
+ }
45
+
46
+ .container {
47
+ max-width: 1200px;
48
+ margin: 0 auto;
49
+ }
50
+
51
+ /* Header Styles */
52
+ header {
53
+ text-align: center;
54
+ margin-bottom: 2.5rem;
55
+ padding: 3rem 2rem;
56
+ background: white;
57
+ border-radius: 20px;
58
+ box-shadow: var(--card-shadow);
59
+ position: relative;
60
+ overflow: hidden;
61
+ }
62
+
63
+ header::before {
64
+ content: '';
65
+ position: absolute;
66
+ top: 0;
67
+ left: 0;
68
+ width: 100%;
69
+ height: 6px;
70
+ background: var(--gradient);
71
+ }
72
+
73
+ .logo {
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ margin-bottom: 1rem;
78
+ }
79
+
80
+ .logo-icon {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ width: 70px;
85
+ height: 70px;
86
+ background: var(--gradient);
87
+ border-radius: 20px;
88
+ margin-right: 15px;
89
+ box-shadow: 0 10px 20px rgba(67, 97, 238, 0.3);
90
+ }
91
+
92
+ .logo-icon i {
93
+ font-size: 2.5rem;
94
+ color: white;
95
+ }
96
+
97
+ h1 {
98
+ color: var(--primary);
99
+ margin-bottom: 0.5rem;
100
+ font-size: 2.8rem;
101
+ font-weight: 800;
102
+ background: var(--gradient);
103
+ -webkit-background-clip: text;
104
+ -webkit-text-fill-color: transparent;
105
+ }
106
+
107
+ .subtitle {
108
+ color: var(--gray);
109
+ font-size: 1.2rem;
110
+ max-width: 700px;
111
+ margin: 0 auto 2rem;
112
+ }
113
+
114
+ .header-controls {
115
+ display: flex;
116
+ justify-content: center;
117
+ align-items: center;
118
+ gap: 15px;
119
+ margin-top: 1.5rem;
120
+ flex-wrap: wrap;
121
+ }
122
+
123
+ .social-buttons {
124
+ display: flex;
125
+ gap: 10px;
126
+ }
127
+
128
+ .btn {
129
+ display: inline-flex;
130
+ align-items: center;
131
+ justify-content: center;
132
+ padding: 12px 24px;
133
+ border-radius: 12px;
134
+ font-size: 1rem;
135
+ font-weight: 600;
136
+ cursor: pointer;
137
+ transition: var(--transition);
138
+ text-decoration: none;
139
+ border: none;
140
+ }
141
+
142
+ .social-btn {
143
+ background: var(--light);
144
+ color: var(--dark);
145
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
146
+ }
147
+
148
+ .social-btn:hover {
149
+ transform: translateY(-3px);
150
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
151
+ }
152
+
153
+ .social-btn.telegram {
154
+ background: #0088cc;
155
+ color: white;
156
+ }
157
+
158
+ .social-btn.whatsapp {
159
+ background: #25D366;
160
+ color: white;
161
+ }
162
+
163
+ .social-btn i {
164
+ margin-right: 8px;
165
+ font-size: 1.1rem;
166
+ }
167
+
168
+ .action-btn {
169
+ background: var(--accent);
170
+ color: white;
171
+ box-shadow: 0 4px 10px rgba(114, 9, 183, 0.3);
172
+ }
173
+
174
+ .action-btn:hover {
175
+ background: var(--accent-light);
176
+ transform: translateY(-3px);
177
+ box-shadow: 0 8px 20px rgba(114, 9, 183, 0.4);
178
+ }
179
+
180
+ .action-btn i {
181
+ margin-right: 8px;
182
+ font-size: 1.1rem;
183
+ }
184
+
185
+ /* Main Content Styles */
186
+ .main-content {
187
+ display: grid;
188
+ grid-template-columns: 1fr 1fr;
189
+ gap: 2rem;
190
+ margin-bottom: 2.5rem;
191
+ }
192
+
193
+ @media (max-width: 992px) {
194
+ .main-content {
195
+ grid-template-columns: 1fr;
196
+ }
197
+ }
198
+
199
+ .card {
200
+ background: white;
201
+ border-radius: 20px;
202
+ padding: 2.5rem;
203
+ box-shadow: var(--card-shadow);
204
+ transition: var(--transition);
205
+ position: relative;
206
+ overflow: hidden;
207
+ height: 100%;
208
+ }
209
+
210
+ .card::before {
211
+ content: '';
212
+ position: absolute;
213
+ top: 0;
214
+ left: 0;
215
+ width: 100%;
216
+ height: 6px;
217
+ background: var(--gradient);
218
+ transform: scaleX(0);
219
+ transform-origin: left;
220
+ transition: transform 0.5s ease;
221
+ }
222
+
223
+ .card:hover::before {
224
+ transform: scaleX(1);
225
+ }
226
+
227
+ .card:hover {
228
+ transform: translateY(-10px);
229
+ box-shadow: var(--card-shadow-hover);
230
+ }
231
+
232
+ .card-title {
233
+ display: flex;
234
+ align-items: center;
235
+ margin-bottom: 1.8rem;
236
+ color: var(--secondary);
237
+ font-size: 1.5rem;
238
+ font-weight: 700;
239
+ }
240
+
241
+ .card-title i {
242
+ margin-right: 12px;
243
+ font-size: 1.8rem;
244
+ color: var(--primary);
245
+ }
246
+
247
+ .input-group {
248
+ margin-bottom: 1.8rem;
249
+ }
250
+
251
+ label {
252
+ display: block;
253
+ margin-bottom: 0.8rem;
254
+ font-weight: 600;
255
+ color: var(--dark);
256
+ }
257
+
258
+ input[type="text"],
259
+ input[type="file"],
260
+ textarea {
261
+ width: 100%;
262
+ padding: 16px 20px;
263
+ border: 1px solid #e2e8f0;
264
+ border-radius: 12px;
265
+ font-size: 1rem;
266
+ transition: var(--transition);
267
+ resize: vertical;
268
+ background: var(--light);
269
+ }
270
+
271
+ input[type="text"]:focus,
272
+ input[type="file"]:focus,
273
+ textarea:focus {
274
+ outline: none;
275
+ border-color: var(--primary);
276
+ box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
277
+ background: white;
278
+ }
279
+
280
+ .file-input-container {
281
+ position: relative;
282
+ overflow: hidden;
283
+ display: inline-block;
284
+ width: 100%;
285
+ }
286
+
287
+ .file-input-container input[type="file"] {
288
+ position: absolute;
289
+ left: 0;
290
+ top: 0;
291
+ opacity: 0;
292
+ cursor: pointer;
293
+ width: 100%;
294
+ height: 100%;
295
+ }
296
+
297
+ .file-input-button {
298
+ display: flex;
299
+ justify-content: space-between;
300
+ align-items: center;
301
+ padding: 16px 20px;
302
+ background-color: var(--light);
303
+ border: 2px dashed var(--gray-light);
304
+ border-radius: 12px;
305
+ color: var(--gray);
306
+ cursor: pointer;
307
+ transition: var(--transition);
308
+ }
309
+
310
+ .file-input-button:hover {
311
+ border-color: var(--primary);
312
+ color: var(--primary);
313
+ background-color: rgba(67, 97, 238, 0.05);
314
+ }
315
+
316
+ .file-name {
317
+ margin-top: 10px;
318
+ font-size: 0.9rem;
319
+ color: var(--gray);
320
+ }
321
+
322
+ .btn-generate {
323
+ background: var(--gradient);
324
+ color: white;
325
+ width: 100%;
326
+ padding: 18px 24px;
327
+ font-size: 1.1rem;
328
+ position: relative;
329
+ overflow: hidden;
330
+ margin-top: 10px;
331
+ }
332
+
333
+ .btn-generate::after {
334
+ content: '';
335
+ position: absolute;
336
+ top: 0;
337
+ left: -100%;
338
+ width: 100%;
339
+ height: 100%;
340
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
341
+ transition: left 0.5s;
342
+ }
343
+
344
+ .btn-generate:hover::after {
345
+ left: 100%;
346
+ }
347
+
348
+ .btn-generate:hover {
349
+ background: var(--gradient);
350
+ box-shadow: 0 10px 25px rgba(67, 97, 238, 0.4);
351
+ transform: translateY(-3px);
352
+ }
353
+
354
+ .btn-secondary {
355
+ background: var(--gradient-accent);
356
+ color: white;
357
+ width: 100%;
358
+ padding: 16px 24px;
359
+ margin-top: 15px;
360
+ }
361
+
362
+ .btn-secondary:hover {
363
+ background: var(--gradient-accent);
364
+ box-shadow: 0 10px 25px rgba(114, 9, 183, 0.4);
365
+ transform: translateY(-3px);
366
+ }
367
+
368
+ .audio-container {
369
+ text-align: center;
370
+ padding: 2rem;
371
+ background: var(--light);
372
+ border-radius: 16px;
373
+ margin-bottom: 2rem;
374
+ }
375
+
376
+ audio {
377
+ width: 100%;
378
+ margin-top: 15px;
379
+ border-radius: 30px;
380
+ }
381
+
382
+ .status-container {
383
+ display: flex;
384
+ align-items: center;
385
+ justify-content: center;
386
+ padding: 1.5rem;
387
+ border-radius: 12px;
388
+ margin-top: 2rem;
389
+ background-color: var(--light);
390
+ min-height: 80px;
391
+ }
392
+
393
+ .status-connected {
394
+ background-color: rgba(76, 201, 240, 0.2);
395
+ color: var(--success);
396
+ border-left: 4px solid var(--success);
397
+ }
398
+
399
+ .status-generating {
400
+ background-color: rgba(247, 37, 133, 0.2);
401
+ color: var(--warning);
402
+ border-left: 4px solid var(--warning);
403
+ }
404
+
405
+ .status-error {
406
+ background-color: rgba(247, 37, 133, 0.2);
407
+ color: var(--warning);
408
+ border-left: 4px solid var(--warning);
409
+ }
410
+
411
+ .status-idle {
412
+ background-color: var(--light);
413
+ color: var(--gray);
414
+ border-left: 4px solid var(--gray-light);
415
+ }
416
+
417
+ .status-icon {
418
+ margin-right: 12px;
419
+ font-size: 1.5rem;
420
+ }
421
+
422
+ /* Benefits Section */
423
+ .benefits-section {
424
+ margin-top: 3rem;
425
+ }
426
+
427
+ .section-title {
428
+ text-align: center;
429
+ margin-bottom: 2.5rem;
430
+ color: var(--dark);
431
+ font-size: 2.2rem;
432
+ font-weight: 700;
433
+ }
434
+
435
+ .benefits-grid {
436
+ display: grid;
437
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
438
+ gap: 2rem;
439
+ }
440
+
441
+ .benefit-card {
442
+ background: white;
443
+ border-radius: 16px;
444
+ padding: 2.5rem 2rem;
445
+ box-shadow: var(--card-shadow);
446
+ transition: var(--transition);
447
+ text-align: center;
448
+ position: relative;
449
+ overflow: hidden;
450
+ }
451
+
452
+ .benefit-card::before {
453
+ content: '';
454
+ position: absolute;
455
+ top: 0;
456
+ left: 0;
457
+ width: 100%;
458
+ height: 5px;
459
+ background: var(--gradient-light);
460
+ }
461
+
462
+ .benefit-card:hover {
463
+ transform: translateY(-10px);
464
+ box-shadow: var(--card-shadow-hover);
465
+ }
466
+
467
+ .benefit-icon {
468
+ display: flex;
469
+ align-items: center;
470
+ justify-content: center;
471
+ width: 80px;
472
+ height: 80px;
473
+ background: var(--gradient-light);
474
+ border-radius: 20px;
475
+ margin: 0 auto 1.5rem;
476
+ color: white;
477
+ font-size: 2rem;
478
+ }
479
+
480
+ .benefit-card h3 {
481
+ margin-bottom: 1rem;
482
+ color: var(--dark);
483
+ font-size: 1.4rem;
484
+ }
485
+
486
+ .benefit-card p {
487
+ color: var(--gray);
488
+ line-height: 1.6;
489
+ }
490
+
491
+ /* Warning Banner */
492
+ .warning-banner {
493
+ background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%);
494
+ color: #d63031;
495
+ padding: 20px 25px;
496
+ border-radius: 12px;
497
+ margin-bottom: 2.5rem;
498
+ display: flex;
499
+ align-items: center;
500
+ box-shadow: 0 4px 10px rgba(214, 48, 49, 0.2);
501
+ }
502
+
503
+ .warning-banner i {
504
+ font-size: 1.8rem;
505
+ margin-right: 20px;
506
+ }
507
+
508
+ .warning-content h3 {
509
+ margin-bottom: 8px;
510
+ font-size: 1.3rem;
511
+ }
512
+
513
+ /* Progress Bar */
514
+ .progress-container {
515
+ width: 100%;
516
+ background-color: #e9ecef;
517
+ border-radius: 10px;
518
+ margin: 20px 0;
519
+ overflow: hidden;
520
+ height: 12px;
521
+ }
522
+
523
+ .progress-bar {
524
+ height: 100%;
525
+ background: var(--gradient);
526
+ width: 0%;
527
+ transition: width 0.3s ease;
528
+ }
529
+
530
+ .progress-text {
531
+ text-align: center;
532
+ font-size: 0.9rem;
533
+ color: var(--gray);
534
+ margin-top: 8px;
535
+ }
536
+
537
+ .hidden {
538
+ display: none;
539
+ }
540
+
541
+ .loader {
542
+ display: inline-block;
543
+ width: 20px;
544
+ height: 20px;
545
+ border: 3px solid rgba(255, 255, 255, 0.3);
546
+ border-radius: 50%;
547
+ border-top-color: white;
548
+ animation: spin 1s ease-in-out infinite;
549
+ margin-right: 10px;
550
+ }
551
+
552
+ @keyframes spin {
553
+ to {
554
+ transform: rotate(360deg);
555
+ }
556
+ }
557
+
558
+ /* File Type Tabs */
559
+ .file-type-tabs {
560
+ display: flex;
561
+ margin-bottom: 15px;
562
+ border-bottom: 1px solid #e2e8f0;
563
+ }
564
+
565
+ .file-type-tab {
566
+ padding: 12px 20px;
567
+ cursor: pointer;
568
+ border-bottom: 3px solid transparent;
569
+ transition: var(--transition);
570
+ font-weight: 500;
571
+ }
572
+
573
+ .file-type-tab.active {
574
+ border-bottom-color: var(--primary);
575
+ color: var(--primary);
576
+ }
577
+
578
+ .file-type-content {
579
+ display: none;
580
+ }
581
+
582
+ .file-type-content.active {
583
+ display: block;
584
+ }
585
+
586
+ /* Modal Styles */
587
+ .modal-overlay {
588
+ display: none;
589
+ position: fixed;
590
+ top: 0;
591
+ left: 0;
592
+ width: 100%;
593
+ height: 100%;
594
+ background: rgba(0, 0, 0, 0.7);
595
+ z-index: 1000;
596
+ align-items: center;
597
+ justify-content: center;
598
+ padding: 20px;
599
+ overflow-y: auto;
600
+ }
601
+
602
+ .modal-content {
603
+ background: white;
604
+ border-radius: 20px;
605
+ width: 100%;
606
+ max-width: 900px;
607
+ max-height: 90vh;
608
+ overflow: hidden;
609
+ box-shadow: 0 25px 80px rgba(0, 0, 0, 0.3);
610
+ position: relative;
611
+ display: flex;
612
+ flex-direction: column;
613
+ }
614
+
615
+ .modal-header {
616
+ display: flex;
617
+ justify-content: space-between;
618
+ align-items: center;
619
+ padding: 25px 30px;
620
+ border-bottom: 1px solid #e2e8f0;
621
+ background: white;
622
+ z-index: 10;
623
+ border-radius: 20px 20px 0 0;
624
+ flex-shrink: 0;
625
+ }
626
+
627
+ .modal-header h2 {
628
+ color: var(--primary);
629
+ font-size: 1.8rem;
630
+ display: flex;
631
+ align-items: center;
632
+ }
633
+
634
+ .modal-header h2 i {
635
+ margin-right: 12px;
636
+ }
637
+
638
+ .close-modal {
639
+ background: none;
640
+ border: none;
641
+ font-size: 1.8rem;
642
+ color: var(--gray);
643
+ cursor: pointer;
644
+ transition: var(--transition);
645
+ width: 40px;
646
+ height: 40px;
647
+ border-radius: 50%;
648
+ display: flex;
649
+ align-items: center;
650
+ justify-content: center;
651
+ }
652
+
653
+ .close-modal:hover {
654
+ background: var(--light);
655
+ color: var(--dark);
656
+ }
657
+
658
+ .modal-body {
659
+ padding: 30px;
660
+ overflow-y: auto;
661
+ flex-grow: 1;
662
+ display: flex;
663
+ flex-direction: column;
664
+ }
665
+
666
+ /* Voice Gallery Styles */
667
+ .gallery-controls {
668
+ display: flex;
669
+ justify-content: space-between;
670
+ align-items: center;
671
+ margin-bottom: 25px;
672
+ flex-wrap: wrap;
673
+ gap: 15px;
674
+ flex-shrink: 0;
675
+ }
676
+
677
+ .search-box {
678
+ position: relative;
679
+ flex: 1;
680
+ max-width: 400px;
681
+ }
682
+
683
+ .search-box i {
684
+ position: absolute;
685
+ left: 15px;
686
+ top: 50%;
687
+ transform: translateY(-50%);
688
+ color: var(--gray);
689
+ }
690
+
691
+ .search-box input {
692
+ width: 100%;
693
+ padding: 12px 15px 12px 45px;
694
+ border: 1px solid #e2e8f0;
695
+ border-radius: 10px;
696
+ font-size: 1rem;
697
+ transition: var(--transition);
698
+ }
699
+
700
+ .search-box input:focus {
701
+ outline: none;
702
+ border-color: var(--primary);
703
+ box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
704
+ }
705
+
706
+ .voice-grid-container {
707
+ overflow-y: auto;
708
+ flex-grow: 1;
709
+ padding-right: 5px;
710
+ }
711
+
712
+ .voice-grid {
713
+ display: grid;
714
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
715
+ gap: 25px;
716
+ }
717
+
718
+ .voice-card {
719
+ background: white;
720
+ border-radius: 16px;
721
+ overflow: hidden;
722
+ box-shadow: var(--card-shadow);
723
+ transition: var(--transition);
724
+ border: 1px solid #e2e8f0;
725
+ }
726
+
727
+ .voice-card:hover {
728
+ transform: translateY(-5px);
729
+ box-shadow: var(--card-shadow-hover);
730
+ }
731
+
732
+ .voice-header {
733
+ padding: 20px;
734
+ border-bottom: 1px solid #e2e8f0;
735
+ }
736
+
737
+ .voice-title {
738
+ font-size: 1.3rem;
739
+ font-weight: 600;
740
+ margin-bottom: 5px;
741
+ color: var(--dark);
742
+ }
743
+
744
+ .voice-meta {
745
+ display: flex;
746
+ justify-content: space-between;
747
+ color: var(--gray);
748
+ font-size: 0.9rem;
749
+ }
750
+
751
+ .voice-body {
752
+ padding: 20px;
753
+ }
754
+
755
+ .voice-text {
756
+ margin-bottom: 20px;
757
+ color: var(--dark);
758
+ line-height: 1.5;
759
+ max-height: 80px;
760
+ overflow-y: auto;
761
+ position: relative;
762
+ padding-right: 5px;
763
+ }
764
+
765
+ .audio-player {
766
+ width: 100%;
767
+ margin-bottom: 20px;
768
+ border-radius: 30px;
769
+ }
770
+
771
+ .voice-actions {
772
+ display: flex;
773
+ justify-content: space-between;
774
+ gap: 10px;
775
+ }
776
+
777
+ .action-btn-small {
778
+ display: flex;
779
+ align-items: center;
780
+ justify-content: center;
781
+ padding: 10px 15px;
782
+ background: var(--light);
783
+ border: none;
784
+ border-radius: 8px;
785
+ color: var(--dark);
786
+ font-weight: 500;
787
+ cursor: pointer;
788
+ transition: var(--transition);
789
+ flex: 1;
790
+ }
791
+
792
+ .action-btn-small:hover {
793
+ background: var(--primary);
794
+ color: white;
795
+ transform: translateY(-2px);
796
+ }
797
+
798
+ .action-btn-small i {
799
+ margin-right: 8px;
800
+ }
801
+
802
+ .action-btn-small.play-btn.playing {
803
+ background: var(--primary);
804
+ color: white;
805
+ }
806
+
807
+ .empty-state {
808
+ text-align: center;
809
+ padding: 60px 20px;
810
+ color: var(--gray);
811
+ grid-column: 1 / -1;
812
+ }
813
+
814
+ .empty-state i {
815
+ font-size: 4rem;
816
+ margin-bottom: 20px;
817
+ color: var(--light);
818
+ }
819
+
820
+ .empty-state h3 {
821
+ font-size: 1.5rem;
822
+ margin-bottom: 10px;
823
+ }
824
+
825
+ /* Help Modal */
826
+ .help-modal {
827
+ max-width: 700px;
828
+ }
829
+
830
+ .help-steps {
831
+ margin-bottom: 25px;
832
+ }
833
+
834
+ .help-step {
835
+ display: flex;
836
+ margin-bottom: 25px;
837
+ }
838
+
839
+ .step-number {
840
+ display: flex;
841
+ align-items: center;
842
+ justify-content: center;
843
+ width: 40px;
844
+ height: 40px;
845
+ background: var(--primary);
846
+ color: white;
847
+ border-radius: 50%;
848
+ font-weight: bold;
849
+ margin-right: 20px;
850
+ flex-shrink: 0;
851
+ font-size: 1.2rem;
852
+ }
853
+
854
+ .step-content h4 {
855
+ margin-bottom: 8px;
856
+ color: var(--dark);
857
+ font-size: 1.3rem;
858
+ }
859
+
860
+ .step-content p {
861
+ color: var(--gray);
862
+ font-size: 1rem;
863
+ line-height: 1.6;
864
+ }
865
+
866
+ .help-tips {
867
+ background: rgba(67, 97, 238, 0.05);
868
+ padding: 25px;
869
+ border-radius: 12px;
870
+ border-left: 4px solid var(--primary);
871
+ }
872
+
873
+ .help-tips h4 {
874
+ margin-bottom: 15px;
875
+ color: var(--dark);
876
+ display: flex;
877
+ align-items: center;
878
+ font-size: 1.3rem;
879
+ }
880
+
881
+ .help-tips h4 i {
882
+ margin-right: 10px;
883
+ color: var(--primary);
884
+ }
885
+
886
+ .help-tips ul {
887
+ padding-left: 20px;
888
+ color: var(--gray);
889
+ }
890
+
891
+ .help-tips li {
892
+ margin-bottom: 10px;
893
+ line-height: 1.5;
894
+ }
895
+
896
+ /* Voice Samples Modal */
897
+ .samples-grid {
898
+ display: grid;
899
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
900
+ gap: 25px;
901
+ }
902
+
903
+ .sample-card {
904
+ background: white;
905
+ border-radius: 16px;
906
+ padding: 25px;
907
+ box-shadow: var(--card-shadow);
908
+ transition: var(--transition);
909
+ display: flex;
910
+ flex-direction: column;
911
+ border: 1px solid #e2e8f0;
912
+ }
913
+
914
+ .sample-card:hover {
915
+ transform: translateY(-5px);
916
+ box-shadow: var(--card-shadow-hover);
917
+ }
918
+
919
+ .sample-header {
920
+ display: flex;
921
+ align-items: center;
922
+ margin-bottom: 20px;
923
+ }
924
+
925
+ .sample-avatar {
926
+ width: 60px;
927
+ height: 60px;
928
+ border-radius: 50%;
929
+ background: var(--gradient);
930
+ display: flex;
931
+ align-items: center;
932
+ justify-content: center;
933
+ color: white;
934
+ font-size: 1.5rem;
935
+ margin-right: 15px;
936
+ }
937
+
938
+ .sample-info h4 {
939
+ margin-bottom: 5px;
940
+ color: var(--dark);
941
+ font-size: 1.2rem;
942
+ }
943
+
944
+ .sample-info p {
945
+ color: var(--gray);
946
+ font-size: 0.9rem;
947
+ }
948
+
949
+ .sample-audio {
950
+ width: 100%;
951
+ margin-top: 15px;
952
+ border-radius: 20px;
953
+ }
954
+
955
+ /* Responsive Design */
956
+ @media (max-width: 768px) {
957
+ h1 {
958
+ font-size: 2.2rem;
959
+ }
960
+
961
+ .main-content {
962
+ gap: 1.5rem;
963
+ }
964
+
965
+ .card {
966
+ padding: 2rem 1.5rem;
967
+ }
968
+
969
+ .gallery-controls {
970
+ flex-direction: column;
971
+ align-items: stretch;
972
+ }
973
+
974
+ .search-box {
975
+ max-width: 100%;
976
+ }
977
+
978
+ .voice-actions {
979
+ flex-direction: column;
980
+ }
981
+
982
+ .help-step {
983
+ flex-direction: column;
984
+ }
985
+
986
+ .step-number {
987
+ margin-right: 0;
988
+ margin-bottom: 15px;
989
+ }
990
+
991
+ .file-type-tabs {
992
+ flex-direction: column;
993
+ }
994
+
995
+ .file-type-tab {
996
+ border-bottom: none;
997
+ border-left: 3px solid transparent;
998
+ }
999
+
1000
+ .file-type-tab.active {
1001
+ border-left-color: var(--primary);
1002
+ border-bottom-color: transparent;
1003
+ }
1004
+
1005
+ .modal-body {
1006
+ padding: 20px;
1007
+ }
1008
+
1009
+ .modal-header {
1010
+ padding: 20px;
1011
+ }
1012
+
1013
+ .samples-grid {
1014
+ grid-template-columns: 1fr;
1015
+ }
1016
+ }
1017
+
1018
+ @media (max-width: 576px) {
1019
+ .voice-grid {
1020
+ grid-template-columns: 1fr;
1021
+ }
1022
+
1023
+ .benefits-grid {
1024
+ grid-template-columns: 1fr;
1025
+ }
1026
+
1027
+ .header-controls {
1028
+ flex-direction: column;
1029
+ width: 100%;
1030
+ }
1031
+
1032
+ .social-buttons {
1033
+ width: 100%;
1034
+ justify-content: center;
1035
+ }
1036
+
1037
+ .social-btn, .action-btn {
1038
+ width: 100%;
1039
+ justify-content: center;
1040
+ }
1041
+ }
1042
+ </style>
1043
+ </head>
1044
+
1045
+ <body>
1046
+ <div class="container" id="meo" data-link="<% options.user_id %>">
1047
+ <header>
1048
+ <div class="logo">
1049
+ <div class="logo-icon">
1050
+ <i class="fas fa-microphone-alt"></i>
1051
+ </div>
1052
+ <div>
1053
+ <h1>VoxAI Pro</h1>
1054
+ <p class="subtitle">Transform text into natural sounding speech with advanced AI voice cloning technology</p>
1055
+ </div>
1056
+ </div>
1057
+
1058
+ <div class="header-controls">
1059
+ <div class="social-buttons">
1060
+ <a href="https://t.me/voxai_pro" class="btn social-btn telegram" target="_blank">
1061
+ <i class="fab fa-telegram"></i> Join Telegram
1062
+ </a>
1063
+ <a href="https://whatsapp.com/channel/0029Vb6DaK8AzNbtUrfZIH2H" class="btn social-btn whatsapp" target="_blank">
1064
+ <i class="fab fa-whatsapp"></i> Join WhatsApp
1065
+ </a>
1066
+ </div>
1067
+ <button class="btn action-btn" id="helpBtn">
1068
+ <i class="fas fa-question-circle"></i> How to Use
1069
+ </button>
1070
+ <button class="btn action-btn" id="samplesBtn">
1071
+ <i class="fas fa-headphones"></i> Voice Samples
1072
+ </button>
1073
+ </div>
1074
+ </header>
1075
+
1076
+ <div class="warning-banner">
1077
+ <i class="fas fa-exclamation-triangle"></i>
1078
+ <div class="warning-content">
1079
+ <h3>Voice Cloning Notice</h3>
1080
+ <p>Voice cloning may take several minutes for audio longer than 10 seconds. You can close this page and return later - your generated voice will be available in the Voice Gallery when ready.</p>
1081
+ </div>
1082
+ </div>
1083
+
1084
+ <div class="main-content">
1085
+ <div class="card">
1086
+ <div class="card-title">
1087
+ <i class="fas fa-pencil-alt"></i> Text Input
1088
+ </div>
1089
+ <div class="input-group">
1090
+ <label for="textInput">Text to Synthesize</label>
1091
+ <textarea id="textInput" rows="4" placeholder="Enter the text you want to convert to speech"></textarea>
1092
+ </div>
1093
+
1094
+ <div class="input-group">
1095
+ <label>Reference Input</label>
1096
+ <div class="file-type-tabs">
1097
+ <div class="file-type-tab active" data-tab="audio">Audio File</div>
1098
+ <div class="file-type-tab" data-tab="video">Video File</div>
1099
+ </div>
1100
+
1101
+ <div class="file-type-content active" id="audio-tab">
1102
+ <div class="file-input-container">
1103
+ <div class="file-input-button">
1104
+ <span>Choose audio file (MP3, WAV, OGG)</span>
1105
+ <i class="fas fa-upload"></i>
1106
+ </div>
1107
+ <input type="file" id="audioInput" accept="audio/*">
1108
+ </div>
1109
+ <div id="audioFileName" class="file-name">No file selected</div>
1110
+ </div>
1111
+
1112
+ <div class="file-type-content" id="video-tab">
1113
+ <div class="file-input-container">
1114
+ <div class="file-input-button">
1115
+ <span>Choose video file (MP4, AVI, MOV)</span>
1116
+ <i class="fas fa-upload"></i>
1117
+ </div>
1118
+ <input type="file" id="videoInput" accept="video/*">
1119
+ </div>
1120
+ <div id="videoFileName" class="file-name">No file selected</div>
1121
+ </div>
1122
+ </div>
1123
+
1124
+ <button id="generateBtn" class="btn btn-generate">
1125
+ <i class="fas fa-magic"></i> Generate Voice
1126
+ </button>
1127
+
1128
+ <div class="progress-container hidden" id="progressContainer">
1129
+ <div class="progress-bar" id="progressBar"></div>
1130
+ </div>
1131
+ <div class="progress-text hidden" id="progressText">Processing...</div>
1132
+
1133
+ <button id="galleryBtn" class="btn btn-secondary">
1134
+ <i class="fas fa-images"></i> View Voice Gallery
1135
+ </button>
1136
+ </div>
1137
+
1138
+ <div class="card">
1139
+ <div class="card-title">
1140
+ <i class="fas fa-music"></i> Output
1141
+ </div>
1142
+ <div class="audio-container">
1143
+ <p>Your generated audio will appear here</p>
1144
+ <audio id="outputAudio" controls></audio>
1145
+ </div>
1146
+
1147
+ <div class="card-title">
1148
+ <i class="fas fa-bolt"></i> Quick Start
1149
+ </div>
1150
+ <p>New to voice cloning? Try these steps:</p>
1151
+ <ol style="margin: 15px 0; padding-left: 20px; color: var(--gray);">
1152
+ <li style="margin-bottom: 10px;">Enter text you want to convert to speech</li>
1153
+ <li style="margin-bottom: 10px;">Upload a clear audio or video sample of the voice</li>
1154
+ <li style="margin-bottom: 10px;">Click "Generate Voice" and wait for processing</li>
1155
+ <li>Access your generated voices in the Voice Gallery</li>
1156
+ </ol>
1157
+ <p>Need more guidance? Click the "How to Use" button above.</p>
1158
+ </div>
1159
+ </div>
1160
+
1161
+ <div class="status-container status-idle" id="status">
1162
+ <i class="fas fa-info-circle status-icon"></i>
1163
+ <span id="statusText">Ready to connect to backend</span>
1164
+ </div>
1165
+ </div>
1166
+
1167
+ <!-- Voice Gallery Modal -->
1168
+ <div class="modal-overlay" id="galleryModal">
1169
+ <div class="modal-content">
1170
+ <div class="modal-header">
1171
+ <h2><i class="fas fa-images"></i> Voice Gallery</h2>
1172
+ <button class="close-modal" id="closeGalleryModal">
1173
+ <i class="fas fa-times"></i>
1174
+ </button>
1175
+ </div>
1176
+ <div class="modal-body">
1177
+ <div class="gallery-controls">
1178
+ <div class="search-box">
1179
+ <i class="fas fa-search"></i>
1180
+ <input type="text" id="gallerySearch" placeholder="Search voices...">
1181
+ </div>
1182
+ </div>
1183
+
1184
+ <div class="voice-grid-container">
1185
+ <div class="voice-grid" id="voiceGrid">
1186
+ <!-- Voice cards will be populated by JavaScript -->
1187
+ </div>
1188
+ </div>
1189
+ </div>
1190
+ </div>
1191
+ </div>
1192
+
1193
+ <!-- Help Modal -->
1194
+ <div class="modal-overlay" id="helpModal">
1195
+ <div class="modal-content help-modal">
1196
+ <div class="modal-header">
1197
+ <h2><i class="fas fa-question-circle"></i> How to Clone Voices</h2>
1198
+ <button class="close-modal" id="closeHelpModal">
1199
+ <i class="fas fa-times"></i>
1200
+ </button>
1201
+ </div>
1202
+ <div class="modal-body">
1203
+ <div class="help-steps">
1204
+ <div class="help-step">
1205
+ <div class="step-number">1</div>
1206
+ <div class="step-content">
1207
+ <h4>Enter Your Text</h4>
1208
+ <p>Type or paste the text you want to convert to speech in the text area. This will be the content spoken in the cloned voice. You can enter anything from a short phrase to several paragraphs.</p>
1209
+ </div>
1210
+ </div>
1211
+ <div class="help-step">
1212
+ <div class="step-number">2</div>
1213
+ <div class="step-content">
1214
+ <h4>Upload Reference Audio or Video</h4>
1215
+ <p>Select either an audio file (MP3, WAV, OGG) or a video file (MP4, AVI, MOV) that contains the voice you want to clone. The clearer the audio, the better the results. For best quality, use samples with minimal background noise.</p>
1216
+ </div>
1217
+ </div>
1218
+ <div class="help-step">
1219
+ <div class="step-number">3</div>
1220
+ <div class="step-content">
1221
+ <h4>Generate the Voice</h4>
1222
+ <p>Click the "Generate Voice" button. The AI will analyze the reference voice and create a clone that speaks your text. Processing time varies based on audio length and complexity.</p>
1223
+ </div>
1224
+ </div>
1225
+ <div class="help-step">
1226
+ <div class="step-number">4</div>
1227
+ <div class="step-content">
1228
+ <h4>Access Your Voices</h4>
1229
+ <p>All your generated voices are saved in the Voice Gallery. You can play, download, or share them anytime. Your voices remain accessible even after closing the browser.</p>
1230
+ </div>
1231
+ </div>
1232
+ </div>
1233
+ <div class="help-tips">
1234
+ <h4><i class="fas fa-lightbulb"></i> Tips for Best Results</h4>
1235
+ <ul>
1236
+ <li>Use clear audio with minimal background noise for best quality</li>
1237
+ <li>Reference audio should be at least 10 seconds long for accurate cloning</li>
1238
+ <li>For videos, ensure the person is speaking clearly and directly</li>
1239
+ <li>Longer processing times may be needed for complex voices or longer texts</li>
1240
+ <li>You can close the page and return later - voices are saved automatically</li>
1241
+ <li>Try our voice samples first to understand the quality you can expect</li>
1242
+ </ul>
1243
+ </div>
1244
+ </div>
1245
+ </div>
1246
+ </div>
1247
+
1248
+ <!-- Voice Samples Modal -->
1249
+ <div class="modal-overlay" id="samplesModal">
1250
+ <div class="modal-content">
1251
+ <div class="modal-header">
1252
+ <h2><i class="fas fa-headphones"></i> Voice Samples</h2>
1253
+ <button class="close-modal" id="closeSamplesModal">
1254
+ <i class="fas fa-times"></i>
1255
+ </button>
1256
+ </div>
1257
+ <div class="modal-body">
1258
+ <p style="margin-bottom: 25px; color: var(--gray); text-align: center;">Listen to these samples to understand the quality of voices you can create with VoxAI Pro.</p>
1259
+ <div class="samples-grid">
1260
+ <div class="sample-card">
1261
+ <div class="sample-header">
1262
+ <div class="sample-avatar">
1263
+ <i class="fas fa-user"></i>
1264
+ </div>
1265
+ <div class="sample-info">
1266
+ <h4>Male Voice - Professional</h4>
1267
+ <p>Clear, authoritative tone</p>
1268
+ </div>
1269
+ </div>
1270
+ <audio class="sample-audio" controls>
1271
+ <source src="#" type="audio/mpeg">
1272
+ Your browser does not support the audio element.
1273
+ </audio>
1274
+ </div>
1275
+
1276
+ <div class="sample-card">
1277
+ <div class="sample-header">
1278
+ <div class="sample-avatar">
1279
+ <i class="fas fa-user-female"></i>
1280
+ </div>
1281
+ <div class="sample-info">
1282
+ <h4>Female Voice - Conversational</h4>
1283
+ <p>Friendly, engaging tone</p>
1284
+ </div>
1285
+ </div>
1286
+ <audio class="sample-audio" controls>
1287
+ <source src="#" type="audio/mpeg">
1288
+ Your browser does not support the audio element.
1289
+ </audio>
1290
+ </div>
1291
+
1292
+ <div class="sample-card">
1293
+ <div class="sample-header">
1294
+ <div class="sample-avatar">
1295
+ <i class="fas fa-user-tie"></i>
1296
+ </div>
1297
+ <div class="sample-info">
1298
+ <h4>Male Voice - Narration</h4>
1299
+ <p>Storytelling style</p>
1300
+ </div>
1301
+ </div>
1302
+ <audio class="sample-audio" controls>
1303
+ <source src="#" type="audio/mpeg">
1304
+ Your browser does not support the audio element.
1305
+ </audio>
1306
+ </div>
1307
+
1308
+ <div class="sample-card">
1309
+ <div class="sample-header">
1310
+ <div class="sample-avatar">
1311
+ <i class="fas fa-user-graduate"></i>
1312
+ </div>
1313
+ <div class="sample-info">
1314
+ <h4>Female Voice - Educational</h4>
1315
+ <p>Clear, instructional tone</p>
1316
+ </div>
1317
+ </div>
1318
+ <audio class="sample-audio" controls>
1319
+ <source src="#" type="audio/mpeg">
1320
+ Your browser does not support the audio element.
1321
+ </audio>
1322
+ </div>
1323
+ </div>
1324
+ </div>
1325
+ </div>
1326
+ </div>
1327
+
1328
+ <!-- Notification Container -->
1329
+ <div id="notificationContainer"></div>
1330
+
1331
+ <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
1332
+ <script>
1333
+ // DOM Elements for main app
1334
+ const socket = io('https://voxai-api3.onrender.com');
1335
+ const textInput = document.getElementById('textInput');
1336
+ const audioInput = document.getElementById('audioInput');
1337
+ const videoInput = document.getElementById('videoInput');
1338
+ const generateBtn = document.getElementById('generateBtn');
1339
+ const galleryBtn = document.getElementById('galleryBtn');
1340
+ const helpBtn = document.getElementById('helpBtn');
1341
+ const samplesBtn = document.getElementById('samplesBtn');
1342
+ const outputAudio = document.getElementById('outputAudio');
1343
+ const status = document.getElementById('status');
1344
+ let statusText = document.getElementById('statusText');
1345
+ const audioFileName = document.getElementById('audioFileName');
1346
+ const videoFileName = document.getElementById('videoFileName');
1347
+ const notificationContainer = document.getElementById('notificationContainer');
1348
+ const progressContainer = document.getElementById('progressContainer');
1349
+ const progressBar = document.getElementById('progressBar');
1350
+ const progressText = document.getElementById('progressText');
1351
+
1352
+ // Modal elements
1353
+ const galleryModal = document.getElementById('galleryModal');
1354
+ const closeGalleryModal = document.getElementById('closeGalleryModal');
1355
+ const helpModal = document.getElementById('helpModal');
1356
+ const closeHelpModal = document.getElementById('closeHelpModal');
1357
+ const samplesModal = document.getElementById('samplesModal');
1358
+ const closeSamplesModal = document.getElementById('closeSamplesModal');
1359
+
1360
+ // File type tabs
1361
+ const fileTypeTabs = document.querySelectorAll('.file-type-tab');
1362
+ const fileTypeContents = document.querySelectorAll('.file-type-content');
1363
+
1364
+ // API URLs
1365
+ const api_url = "https://voxai-api2.onrender.com/generate_voice";
1366
+ const getMyTask = "https://voxai-api2.onrender.com/task_status";
1367
+
1368
+ // State Management
1369
+ let isGenerating = false;
1370
+ let pollingInterval = null;
1371
+ let currentAudio = null;
1372
+ const user_id = document.getElementById('meo').dataset.link;
1373
+
1374
+ // Voice data storage
1375
+ let voiceData = [];
1376
+
1377
+ // File type management
1378
+ let currentFileType = 'audio';
1379
+
1380
+ // Initialize file type tabs
1381
+ fileTypeTabs.forEach(tab => {
1382
+ tab.addEventListener('click', () => {
1383
+ const tabType = tab.getAttribute('data-tab');
1384
+
1385
+ // Update active tab
1386
+ fileTypeTabs.forEach(t => t.classList.remove('active'));
1387
+ tab.classList.add('active');
1388
+
1389
+ // Show corresponding content
1390
+ fileTypeContents.forEach(content => {
1391
+ content.classList.remove('active');
1392
+ if (content.id === `${tabType}-tab`) {
1393
+ content.classList.add('active');
1394
+ }
1395
+ });
1396
+
1397
+ currentFileType = tabType;
1398
+ });
1399
+ });
1400
+
1401
+ // Socket connection and event handlers
1402
+ socket.on('connect', () => {
1403
+ console.log('Connected to server');
1404
+ updateStatus('Connected to voice service', 'connected');
1405
+
1406
+ // Request initial gallery data
1407
+ socket.emit("audio_gallery", {user_id});
1408
+ });
1409
+
1410
+ socket.on('disconnect', () => {
1411
+ console.log('Disconnected from server');
1412
+ updateStatus('Disconnected from voice service', 'error');
1413
+ });
1414
+
1415
+ socket.on('your_audios', (data) => {
1416
+ console.log('Received audio gallery data:', data);
1417
+ if (data.audios && Array.isArray(data.audios)) {
1418
+ voiceData = data.audios;
1419
+ renderVoiceCards(voiceData);
1420
+ }
1421
+ });
1422
+
1423
+ socket.on('new_audio', (data) => {
1424
+ console.log('New audio received:', data);
1425
+ if (data.audio) {
1426
+ // Add new audio to the gallery
1427
+ voiceData.unshift(data.audio);
1428
+ renderVoiceCards(voiceData);
1429
+
1430
+ // Show notification if gallery is open
1431
+ if (galleryModal.style.display === 'flex') {
1432
+ showNotification('New voice added to gallery!', 'success');
1433
+ }
1434
+ }
1435
+ });
1436
+
1437
+ socket.on('audio_update', (data) => {
1438
+ console.log('Audio updated:', data);
1439
+ if (data.audio) {
1440
+ // Update existing audio in the gallery
1441
+ const index = voiceData.findIndex(v => v.voice_id === data.audio.voice_id);
1442
+ if (index !== -1) {
1443
+ voiceData[index] = data.audio;
1444
+ renderVoiceCards(voiceData);
1445
+ }
1446
+ }
1447
+ });
1448
+
1449
+ // Utility Functions for main app
1450
+ const isValidAudioFile = (file) => {
1451
+ const allowedTypes = ['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/mp4', 'audio/x-m4a', 'audio/mp3'];
1452
+ return file && allowedTypes.includes(file.type);
1453
+ };
1454
+
1455
+ const isValidVideoFile = (file) => {
1456
+ const allowedTypes = ['video/mp4', 'video/avi', 'video/mov', 'video/webm', 'video/quicktime'];
1457
+ return file && allowedTypes.includes(file.type);
1458
+ };
1459
+
1460
+ const displayFileName = (filename, type) => {
1461
+ if (type === 'audio') {
1462
+ audioFileName.textContent = filename || 'No file selected';
1463
+ } else if (type === 'video') {
1464
+ videoFileName.textContent = filename || 'No file selected';
1465
+ }
1466
+ };
1467
+
1468
+ const updateButtonState = (loading) => {
1469
+ isGenerating = loading;
1470
+ generateBtn.disabled = loading;
1471
+ generateBtn.innerHTML = loading
1472
+ ? '<div class="loader"></div> Processing...'
1473
+ : '<i class="fas fa-magic"></i> Generate Voice';
1474
+ };
1475
+
1476
+ const updateStatus = (message, type) => {
1477
+ statusText.textContent = message;
1478
+ status.className = 'status-container'; // Reset classes
1479
+ let iconClass = 'fas fa-info-circle';
1480
+
1481
+ switch (type) {
1482
+ case 'connected':
1483
+ status.classList.add('status-connected');
1484
+ iconClass = 'fas fa-check-circle';
1485
+ break;
1486
+ case 'generating':
1487
+ status.classList.add('status-generating');
1488
+ iconClass = 'fas fa-cog fa-spin';
1489
+ break;
1490
+ case 'error':
1491
+ status.classList.add('status-error');
1492
+ iconClass = 'fas fa-exclamation-circle';
1493
+ break;
1494
+ default:
1495
+ status.classList.add('status-idle');
1496
+ }
1497
+
1498
+ status.innerHTML = `<i class="${iconClass} status-icon"></i> <span id="statusText">${message}</span>`;
1499
+ // Re-reference statusText after updating innerHTML
1500
+ statusText = document.getElementById('statusText');
1501
+ };
1502
+
1503
+ const updateProgress = (percentage, message) => {
1504
+ if (percentage === 0) {
1505
+ progressContainer.classList.remove('hidden');
1506
+ progressText.classList.remove('hidden');
1507
+ }
1508
+
1509
+ progressBar.style.width = `${percentage}%`;
1510
+ progressText.textContent = message || `Processing... ${percentage}%`;
1511
+
1512
+ if (percentage >= 100) {
1513
+ setTimeout(() => {
1514
+ progressContainer.classList.add('hidden');
1515
+ progressText.classList.add('hidden');
1516
+ }, 1000);
1517
+ }
1518
+ };
1519
+
1520
+ const stopPolling = () => {
1521
+ if (pollingInterval) {
1522
+ clearInterval(pollingInterval);
1523
+ pollingInterval = null;
1524
+ }
1525
+ };
1526
+
1527
+ // Notification system
1528
+ function showNotification(message, type = 'info', title = null) {
1529
+ const notification = document.createElement('div');
1530
+ notification.className = `notification ${type}`;
1531
+
1532
+ const icon = type === 'success' ? 'fas fa-check-circle' :
1533
+ type === 'warning' ? 'fas fa-exclamation-triangle' :
1534
+ 'fas fa-info-circle';
1535
+
1536
+ notification.innerHTML = `
1537
+ <i class="${icon} notification-icon"></i>
1538
+ <div class="notification-content">
1539
+ ${title ? `<div class="notification-title">${title}</div>` : ''}
1540
+ <div class="notification-message">${message}</div>
1541
+ </div>
1542
+ <button class="notification-close">
1543
+ <i class="fas fa-times"></i>
1544
+ </button>
1545
+ `;
1546
+
1547
+ notificationContainer.appendChild(notification);
1548
+
1549
+ // Trigger animation
1550
+ setTimeout(() => {
1551
+ notification.classList.add('show');
1552
+ }, 10);
1553
+
1554
+ // Close button event
1555
+ const closeBtn = notification.querySelector('.notification-close');
1556
+ closeBtn.addEventListener('click', () => {
1557
+ closeNotification(notification);
1558
+ });
1559
+
1560
+ // Auto-close after 5 seconds
1561
+ setTimeout(() => {
1562
+ if (notification.parentNode) {
1563
+ closeNotification(notification);
1564
+ }
1565
+ }, 5000);
1566
+ }
1567
+
1568
+ function closeNotification(notification) {
1569
+ notification.classList.remove('show');
1570
+ setTimeout(() => {
1571
+ if (notification.parentNode) {
1572
+ notificationContainer.removeChild(notification);
1573
+ }
1574
+ }, 300);
1575
+ }
1576
+
1577
+ // Event Listeners for main app
1578
+ audioInput.addEventListener('change', function () {
1579
+ const file = this.files[0];
1580
+ if (file) {
1581
+ if (!isValidAudioFile(file)) {
1582
+ updateStatus('Invalid audio file type. Please select MP3, WAV, or OGG.', 'error');
1583
+ this.value = ''; // Clear the file input
1584
+ displayFileName(null, 'audio');
1585
+ return;
1586
+ }
1587
+ displayFileName(file.name, 'audio');
1588
+ updateStatus('Audio file selected successfully', 'connected');
1589
+ } else {
1590
+ displayFileName(null, 'audio');
1591
+ }
1592
+ });
1593
+
1594
+ videoInput.addEventListener('change', function () {
1595
+ const file = this.files[0];
1596
+ if (file) {
1597
+ if (!isValidVideoFile(file)) {
1598
+ updateStatus('Invalid video file type. Please select MP4, AVI, or MOV.', 'error');
1599
+ this.value = ''; // Clear the file input
1600
+ displayFileName(null, 'video');
1601
+ return;
1602
+ }
1603
+ displayFileName(file.name, 'video');
1604
+ updateStatus('Video file selected successfully', 'connected');
1605
+ } else {
1606
+ displayFileName(null, 'video');
1607
+ }
1608
+ });
1609
+
1610
+ generateBtn.addEventListener('click', async () => {
1611
+ const text = textInput.value.trim();
1612
+ let file = null;
1613
+ let fileType = null;
1614
+
1615
+ if (currentFileType === 'audio') {
1616
+ file = audioInput.files[0];
1617
+ fileType = 'audio';
1618
+ } else {
1619
+ file = videoInput.files[0];
1620
+ fileType = 'video';
1621
+ }
1622
+
1623
+ if (isGenerating) return;
1624
+
1625
+ if (!text) {
1626
+ updateStatus('Please enter text to synthesize', 'error');
1627
+ textInput.focus();
1628
+ return;
1629
+ }
1630
+
1631
+ if (!file) {
1632
+ updateStatus(`Please select a ${fileType} file`, 'error');
1633
+ return;
1634
+ }
1635
+
1636
+ if (fileType === 'audio' && !isValidAudioFile(file)) {
1637
+ updateStatus('Invalid audio file type. Please select MP3, WAV, or OGG.', 'error');
1638
+ return;
1639
+ }
1640
+
1641
+ if (fileType === 'video' && !isValidVideoFile(file)) {
1642
+ updateStatus('Invalid video file type. Please select MP4, AVI, or MOV.', 'error');
1643
+ return;
1644
+ }
1645
+
1646
+ try {
1647
+ updateStatus('Generating voice...', 'generating');
1648
+ updateButtonState(true);
1649
+ updateProgress(10, 'Uploading file...');
1650
+ stopPolling(); // Clear any existing polling
1651
+
1652
+ const reader = new FileReader();
1653
+
1654
+ reader.onload = async (e) => {
1655
+ try {
1656
+ const fileBase64 = e.target.result.split(',')[1]; // Remove data URL prefix
1657
+ const task_id = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
1658
+
1659
+ // Update progress
1660
+ updateProgress(30, 'Processing file...');
1661
+
1662
+ // Send generation request
1663
+ const response = await fetch(api_url, {
1664
+ method: 'POST',
1665
+ headers: {
1666
+ 'Content-Type': 'application/json',
1667
+ 'Accept': 'application/json'
1668
+ },
1669
+ body: JSON.stringify({
1670
+ text,
1671
+ [fileType]: fileBase64,
1672
+ task_id,
1673
+ user_id
1674
+ })
1675
+ });
1676
+
1677
+ if (!response.ok) {
1678
+ throw new Error(`HTTP error! status: ${response.status}`);
1679
+ }
1680
+
1681
+ updateProgress(50, 'Generating voice...');
1682
+
1683
+ // Start polling for results
1684
+ pollingInterval = setInterval(async () => {
1685
+ try {
1686
+ const res = await fetch(`${getMyTask}?task_id=${task_id}`);
1687
+
1688
+ if (!res.ok) {
1689
+ throw new Error(`HTTP error! status: ${res.status}`);
1690
+ }
1691
+
1692
+ const data = await res.json();
1693
+
1694
+ if (data.status === "complete") {
1695
+ stopPolling();
1696
+ updateProgress(100, 'Voice generated successfully!');
1697
+
1698
+ // Handle the audio data - now expecting URL format
1699
+ if (data.audio_url) {
1700
+ outputAudio.src = data.audio_url;
1701
+ } else if (data.audio) {
1702
+ // If audio is still in base64 format (fallback)
1703
+ if (data.audio.startsWith('data:')) {
1704
+ outputAudio.src = data.audio;
1705
+ } else {
1706
+ outputAudio.src = `data:audio/wav;base64,${data.audio}`;
1707
+ }
1708
+ }
1709
+ outputAudio.controls = true;
1710
+ updateStatus('Voice generated successfully!', 'connected');
1711
+ updateButtonState(false);
1712
+
1713
+ // Show success notification
1714
+ showNotification('Your voice has been generated successfully!', 'success');
1715
+
1716
+ // Notify server about new audio
1717
+ socket.emit('new_audio_generated', {
1718
+ user_id,
1719
+ audio: {
1720
+ voice_id: task_id,
1721
+ title: "New Generated Voice",
1722
+ text: text,
1723
+ duration: "0:20",
1724
+ created_at: new Date().toISOString(),
1725
+ audio: outputAudio.src
1726
+ }
1727
+ });
1728
+ } else if (data.status === "failed") {
1729
+ stopPolling();
1730
+ updateProgress(0, '');
1731
+ updateStatus(`Voice generation failed: ${data.error || 'Unknown error'}`, 'error');
1732
+ updateButtonState(false);
1733
+ showNotification(`Voice generation failed: ${data.error || 'Unknown error'}`, 'warning');
1734
+ } else {
1735
+ const progress = data.progress || 50;
1736
+ updateProgress(progress, `Processing: ${data.status}`);
1737
+ updateStatus(`Processing: ${data.status}`, 'generating');
1738
+ }
1739
+ } catch (err) {
1740
+ stopPolling();
1741
+ updateProgress(0, '');
1742
+ console.error("Task Status Error:", err);
1743
+ updateStatus(`Error checking task: ${err.message}`, 'error');
1744
+ updateButtonState(false);
1745
+ showNotification(`Error checking task: ${err.message}`, 'warning');
1746
+ }
1747
+ }, 3000);
1748
+
1749
+ } catch (error) {
1750
+ stopPolling();
1751
+ updateProgress(0, '');
1752
+ console.error("API Error:", error);
1753
+ updateStatus(`Voice generation failed: ${error.message}`, 'error');
1754
+ updateButtonState(false);
1755
+ showNotification(`Voice generation failed: ${error.message}`, 'warning');
1756
+ }
1757
+ };
1758
+
1759
+ reader.onerror = () => {
1760
+ stopPolling();
1761
+ updateProgress(0, '');
1762
+ updateStatus('Error reading file', 'error');
1763
+ updateButtonState(false);
1764
+ showNotification('Error reading file', 'warning');
1765
+ };
1766
+
1767
+ reader.readAsDataURL(file);
1768
+ } catch (error) {
1769
+ stopPolling();
1770
+ updateProgress(0, '');
1771
+ console.error("Unexpected Error:", error);
1772
+ updateStatus(`Unexpected error: ${error.message}`, 'error');
1773
+ updateButtonState(false);
1774
+ showNotification(`Unexpected error: ${error.message}`, 'warning');
1775
+ }
1776
+ });
1777
+
1778
+ // Gallery functionality
1779
+ function renderVoiceCards(voices) {
1780
+ const voiceGrid = document.getElementById('voiceGrid');
1781
+ voiceGrid.innerHTML = '';
1782
+
1783
+ if (voices.length === 0) {
1784
+ voiceGrid.innerHTML = `
1785
+ <div class="empty-state">
1786
+ <i class="fas fa-volume-mute"></i>
1787
+ <h3>No voices generated yet</h3>
1788
+ <p>Start creating amazing voice content with VoxAI Pro</p>
1789
+ </div>
1790
+ `;
1791
+ return;
1792
+ }
1793
+
1794
+ voices.forEach(voice => {
1795
+ const voiceCard = document.createElement('div');
1796
+ voiceCard.className = 'voice-card';
1797
+
1798
+ // Use audio URL directly if available, otherwise use placeholder
1799
+ const audioUrl = voice.audio_url || voice.audio || '';
1800
+
1801
+ voiceCard.innerHTML = `
1802
+ <div class="voice-header">
1803
+ <h3 class="voice-title">${voice.title || 'Untitled Voice'}</h3>
1804
+ <div class="voice-meta">
1805
+ <span>${voice.duration || '0:00'}</span>
1806
+ <span>${new Date(voice.created_at).toLocaleDateString()}</span>
1807
+ </div>
1808
+ </div>
1809
+ <div class="voice-body">
1810
+ <div class="voice-text">
1811
+ ${voice.text || 'No text content available'}
1812
+ </div>
1813
+ <audio class="audio-player" src="${audioUrl}"></audio>
1814
+ <div class="voice-actions">
1815
+ <button class="action-btn-small play-btn" data-id="${voice.voice_id}">
1816
+ <i class="fas fa-play"></i> Play
1817
+ </button>
1818
+ <button class="action-btn-small download-btn" data-id="${voice.voice_id}">
1819
+ <i class="fas fa-download"></i> Download
1820
+ </button>
1821
+ <button class="action-btn-small share-btn" data-id="${voice.voice_id}">
1822
+ <i class="fas fa-share-alt"></i> Share
1823
+ </button>
1824
+ </div>
1825
+ </div>
1826
+ `;
1827
+ voiceGrid.appendChild(voiceCard);
1828
+ });
1829
+
1830
+ // Add event listeners to buttons
1831
+ addGalleryEventListeners();
1832
+ }
1833
+
1834
+ function addGalleryEventListeners() {
1835
+ // Play buttons
1836
+ document.querySelectorAll('.play-btn').forEach(btn => {
1837
+ btn.addEventListener('click', function() {
1838
+ const id = this.getAttribute('data-id');
1839
+ togglePlayPause(id);
1840
+ });
1841
+ });
1842
+
1843
+ // Download buttons
1844
+ document.querySelectorAll('.download-btn').forEach(btn => {
1845
+ btn.addEventListener('click', function() {
1846
+ const id = this.getAttribute('data-id');
1847
+ downloadVoice(id);
1848
+ });
1849
+ });
1850
+ }
1851
+
1852
+ function togglePlayPause(id) {
1853
+ const voice = voiceData.find(v => v.voice_id == id);
1854
+ if (!voice) return;
1855
+
1856
+ const audioElement = document.querySelector(`.play-btn[data-id="${id}"]`).closest('.voice-card').querySelector('audio');
1857
+ const playButton = document.querySelector(`.play-btn[data-id="${id}"]`);
1858
+
1859
+ // Stop currently playing audio if different
1860
+ if (currentAudio && currentAudio !== audioElement) {
1861
+ currentAudio.pause();
1862
+ const prevButton = document.querySelector('.play-btn.playing');
1863
+ if (prevButton) {
1864
+ prevButton.classList.remove('playing');
1865
+ prevButton.innerHTML = '<i class="fas fa-play"></i> Play';
1866
+ }
1867
+ }
1868
+
1869
+ if (audioElement.paused) {
1870
+ audioElement.play();
1871
+ playButton.classList.add('playing');
1872
+ playButton.innerHTML = '<i class="fas fa-pause"></i> Pause';
1873
+ currentAudio = audioElement;
1874
+
1875
+ // Reset when audio ends
1876
+ audioElement.onended = function() {
1877
+ playButton.classList.remove('playing');
1878
+ playButton.innerHTML = '<i class="fas fa-play"></i> Play';
1879
+ currentAudio = null;
1880
+ };
1881
+ } else {
1882
+ audioElement.pause();
1883
+ playButton.classList.remove('playing');
1884
+ playButton.innerHTML = '<i class="fas fa-play"></i> Play';
1885
+ currentAudio = null;
1886
+ }
1887
+ }
1888
+
1889
+ function downloadVoice(id) {
1890
+ const voice = voiceData.find(v => v.voice_id == id);
1891
+ if (!voice) return;
1892
+
1893
+ // Use the audio URL directly
1894
+ const audioUrl = `${voice.audio}?download=true`;
1895
+
1896
+ // Create download link
1897
+ const link = document.createElement('a');
1898
+ link.href = audioUrl;
1899
+ link.download = `${(voice.title || 'voice').replace(/\s+/g, '_')}.mp3`;
1900
+ document.body.appendChild(link);
1901
+ link.click();
1902
+ document.body.removeChild(link);
1903
+
1904
+ // Show notification instead of alert
1905
+ showNotification(`Downloading: ${voice.title || 'Untitled Voice'}`, 'info');
1906
+ }
1907
+
1908
+ // Modal controls
1909
+ galleryBtn.addEventListener('click', () => {
1910
+ galleryModal.style.display = 'flex';
1911
+ // Refresh gallery data when opening
1912
+ socket.emit("audio_gallery", {user_id});
1913
+ });
1914
+
1915
+ closeGalleryModal.addEventListener('click', () => {
1916
+ galleryModal.style.display = 'none';
1917
+ // Stop any playing audio when closing modal
1918
+ if (currentAudio) {
1919
+ currentAudio.pause();
1920
+ currentAudio = null;
1921
+ document.querySelectorAll('.play-btn.playing').forEach(btn => {
1922
+ btn.classList.remove('playing');
1923
+ btn.innerHTML = '<i class="fas fa-play"></i> Play';
1924
+ });
1925
+ }
1926
+ });
1927
+
1928
+ helpBtn.addEventListener('click', () => {
1929
+ helpModal.style.display = 'flex';
1930
+ });
1931
+
1932
+ closeHelpModal.addEventListener('click', () => {
1933
+ helpModal.style.display = 'none';
1934
+ });
1935
+
1936
+ samplesBtn.addEventListener('click', () => {
1937
+ samplesModal.style.display = 'flex';
1938
+ });
1939
+
1940
+ closeSamplesModal.addEventListener('click', () => {
1941
+ samplesModal.style.display = 'none';
1942
+ });
1943
+
1944
+ // Close modals when clicking outside
1945
+ window.addEventListener('click', (e) => {
1946
+ if (e.target === galleryModal) {
1947
+ galleryModal.style.display = 'none';
1948
+ if (currentAudio) {
1949
+ currentAudio.pause();
1950
+ currentAudio = null;
1951
+ document.querySelectorAll('.play-btn.playing').forEach(btn => {
1952
+ btn.classList.remove('playing');
1953
+ btn.innerHTML = '<i class="fas fa-play"></i> Play';
1954
+ });
1955
+ }
1956
+ }
1957
+ if (e.target === helpModal) {
1958
+ helpModal.style.display = 'none';
1959
+ }
1960
+ if (e.target === samplesModal) {
1961
+ samplesModal.style.display = 'none';
1962
+ }
1963
+ });
1964
+
1965
+ // Search functionality for gallery
1966
+ document.getElementById('gallerySearch').addEventListener('input', () => {
1967
+ const searchTerm = document.getElementById('gallerySearch').value.toLowerCase();
1968
+ const filteredVoices = voiceData.filter(voice =>
1969
+ (voice.title || '').toLowerCase().includes(searchTerm) ||
1970
+ (voice.text || '').toLowerCase().includes(searchTerm)
1971
+ );
1972
+ renderVoiceCards(filteredVoices);
1973
+ });
1974
+
1975
+ // Clean up on page unload
1976
+ window.addEventListener('beforeunload', stopPolling);
1977
+
1978
+ // Initialize the gallery with empty data
1979
+ renderVoiceCards(voiceData);
1980
+ </script>
1981
+ </body>
1982
+ </html>