Spaces:
Sleeping
Sleeping
feat: add export TXT/SRT/PDF, language detection, translate, shimmer animation
Browse files- backend/app.py +75 -16
- backend/transcriber.py +123 -26
- backend/translator.py +159 -0
- frontend/app.js +326 -76
- frontend/index.html +187 -34
- frontend/style.css +607 -2
- requirements.txt +1 -3
backend/app.py
CHANGED
|
@@ -2,33 +2,30 @@
|
|
| 2 |
# ---------------------------------------------------------
|
| 3 |
# THE SERVER β manages all API routes
|
| 4 |
#
|
| 5 |
-
#
|
| 6 |
-
#
|
| 7 |
-
#
|
| 8 |
-
#
|
| 9 |
-
#
|
| 10 |
-
#
|
| 11 |
# ---------------------------------------------------------
|
| 12 |
|
| 13 |
import os
|
| 14 |
from flask import Flask, request, jsonify, send_from_directory
|
| 15 |
from flask_cors import CORS
|
| 16 |
from transcriber import transcribe_audio
|
|
|
|
| 17 |
|
| 18 |
app = Flask(__name__)
|
| 19 |
CORS(app)
|
| 20 |
|
| 21 |
-
|
| 22 |
-
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 23 |
UPLOAD_FOLDER = os.path.join(BASE_DIR, "uploads")
|
| 24 |
FRONTEND_DIR = os.path.join(BASE_DIR, "frontend")
|
| 25 |
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 26 |
|
| 27 |
|
| 28 |
-
# ββ Serve frontend
|
| 29 |
-
# On HF Spaces, Flask serves both the API AND the frontend HTML
|
| 30 |
-
# So visiting the Space URL shows the VoiceScript UI directly
|
| 31 |
-
|
| 32 |
@app.route("/")
|
| 33 |
def index():
|
| 34 |
return send_from_directory(FRONTEND_DIR, "index.html")
|
|
@@ -38,31 +35,93 @@ def frontend_files(filename):
|
|
| 38 |
return send_from_directory(FRONTEND_DIR, filename)
|
| 39 |
|
| 40 |
|
| 41 |
-
# ββ Health check
|
| 42 |
@app.route("/health")
|
| 43 |
def health():
|
| 44 |
return jsonify({"status": "ok", "app": "VoiceScript"})
|
| 45 |
|
| 46 |
|
| 47 |
-
# ββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
@app.route("/transcribe", methods=["POST"])
|
| 49 |
def transcribe():
|
| 50 |
if "audio" not in request.files:
|
| 51 |
return jsonify({"success": False, "error": "No audio file received."}), 400
|
| 52 |
|
| 53 |
audio_file = request.files["audio"]
|
| 54 |
-
|
| 55 |
if audio_file.filename == "":
|
| 56 |
return jsonify({"success": False, "error": "Empty filename."}), 400
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
file_path = os.path.join(UPLOAD_FOLDER, "temp_audio")
|
| 59 |
audio_file.save(file_path)
|
| 60 |
|
| 61 |
-
result = transcribe_audio(file_path)
|
| 62 |
|
| 63 |
if os.path.exists(file_path):
|
| 64 |
os.remove(file_path)
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
return jsonify(result)
|
| 67 |
|
| 68 |
|
|
|
|
| 2 |
# ---------------------------------------------------------
|
| 3 |
# THE SERVER β manages all API routes
|
| 4 |
#
|
| 5 |
+
# Routes:
|
| 6 |
+
# GET / β serves frontend index.html
|
| 7 |
+
# GET /health β health check
|
| 8 |
+
# POST /transcribe β audio β text (any language)
|
| 9 |
+
# POST /translate β text β translated text
|
| 10 |
+
# GET /languages β returns all supported languages
|
| 11 |
# ---------------------------------------------------------
|
| 12 |
|
| 13 |
import os
|
| 14 |
from flask import Flask, request, jsonify, send_from_directory
|
| 15 |
from flask_cors import CORS
|
| 16 |
from transcriber import transcribe_audio
|
| 17 |
+
from translator import translate_text, get_supported_languages
|
| 18 |
|
| 19 |
app = Flask(__name__)
|
| 20 |
CORS(app)
|
| 21 |
|
| 22 |
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
| 23 |
UPLOAD_FOLDER = os.path.join(BASE_DIR, "uploads")
|
| 24 |
FRONTEND_DIR = os.path.join(BASE_DIR, "frontend")
|
| 25 |
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 26 |
|
| 27 |
|
| 28 |
+
# ββ Serve frontend βββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
| 29 |
@app.route("/")
|
| 30 |
def index():
|
| 31 |
return send_from_directory(FRONTEND_DIR, "index.html")
|
|
|
|
| 35 |
return send_from_directory(FRONTEND_DIR, filename)
|
| 36 |
|
| 37 |
|
| 38 |
+
# ββ Health check βββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
@app.route("/health")
|
| 40 |
def health():
|
| 41 |
return jsonify({"status": "ok", "app": "VoiceScript"})
|
| 42 |
|
| 43 |
|
| 44 |
+
# ββ Get supported languages ββββββββββββββββββββββββββββββββ
|
| 45 |
+
# Frontend calls this on load to populate the language dropdown
|
| 46 |
+
@app.route("/languages", methods=["GET"])
|
| 47 |
+
def languages():
|
| 48 |
+
return jsonify({
|
| 49 |
+
"success": True,
|
| 50 |
+
"languages": get_supported_languages()
|
| 51 |
+
})
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# ββ Transcribe audio βββββββββββββββββββββββββββββββββββββββ
|
| 55 |
+
# mode = "transcribe" β transcribe in original language
|
| 56 |
+
# mode = "translate" β transcribe ANY language β English
|
| 57 |
@app.route("/transcribe", methods=["POST"])
|
| 58 |
def transcribe():
|
| 59 |
if "audio" not in request.files:
|
| 60 |
return jsonify({"success": False, "error": "No audio file received."}), 400
|
| 61 |
|
| 62 |
audio_file = request.files["audio"]
|
|
|
|
| 63 |
if audio_file.filename == "":
|
| 64 |
return jsonify({"success": False, "error": "Empty filename."}), 400
|
| 65 |
|
| 66 |
+
# Get mode from form data β default is "transcribe"
|
| 67 |
+
# "translate_to_english" mode tells Whisper to output English
|
| 68 |
+
# regardless of what language was spoken
|
| 69 |
+
mode = request.form.get("mode", "transcribe")
|
| 70 |
+
|
| 71 |
file_path = os.path.join(UPLOAD_FOLDER, "temp_audio")
|
| 72 |
audio_file.save(file_path)
|
| 73 |
|
| 74 |
+
result = transcribe_audio(file_path, mode=mode)
|
| 75 |
|
| 76 |
if os.path.exists(file_path):
|
| 77 |
os.remove(file_path)
|
| 78 |
|
| 79 |
+
# Format segments into clean timestamped lines for frontend
|
| 80 |
+
# Each segment: { start, end, text }
|
| 81 |
+
# We convert seconds β [MM:SS] format here in backend
|
| 82 |
+
if result.get("success") and result.get("segments"):
|
| 83 |
+
timestamped = []
|
| 84 |
+
for seg in result["segments"]:
|
| 85 |
+
start_sec = int(seg.get("start", 0))
|
| 86 |
+
end_sec = int(seg.get("end", 0))
|
| 87 |
+
text = seg.get("text", "").strip()
|
| 88 |
+
if text:
|
| 89 |
+
start_fmt = f"{start_sec // 60:02d}:{start_sec % 60:02d}"
|
| 90 |
+
end_fmt = f"{end_sec // 60:02d}:{end_sec % 60:02d}"
|
| 91 |
+
timestamped.append({
|
| 92 |
+
"start" : start_sec,
|
| 93 |
+
"end" : end_sec,
|
| 94 |
+
"start_fmt" : start_fmt,
|
| 95 |
+
"end_fmt" : end_fmt,
|
| 96 |
+
"text" : text
|
| 97 |
+
})
|
| 98 |
+
result["timestamped"] = timestamped
|
| 99 |
+
# Remove raw segments from response (too heavy)
|
| 100 |
+
del result["segments"]
|
| 101 |
+
|
| 102 |
+
return jsonify(result)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# ββ Translate transcript βββββββββββββββββββββββββββββββββββ
|
| 106 |
+
# Takes existing transcript text + target language code
|
| 107 |
+
# Returns translated text
|
| 108 |
+
@app.route("/translate", methods=["POST"])
|
| 109 |
+
def translate():
|
| 110 |
+
data = request.get_json()
|
| 111 |
+
|
| 112 |
+
if not data:
|
| 113 |
+
return jsonify({"success": False, "error": "No data received."}), 400
|
| 114 |
+
|
| 115 |
+
text = data.get("text", "").strip()
|
| 116 |
+
target_lang = data.get("target_language", "")
|
| 117 |
+
|
| 118 |
+
if not text:
|
| 119 |
+
return jsonify({"success": False, "error": "No text to translate."}), 400
|
| 120 |
+
|
| 121 |
+
if not target_lang:
|
| 122 |
+
return jsonify({"success": False, "error": "No target language specified."}), 400
|
| 123 |
+
|
| 124 |
+
result = translate_text(text, target_lang)
|
| 125 |
return jsonify(result)
|
| 126 |
|
| 127 |
|
backend/transcriber.py
CHANGED
|
@@ -185,7 +185,7 @@ def convert_to_whisper_wav(input_path, output_path):
|
|
| 185 |
# STEP 3 β WHISPER TRANSCRIPTION (medium model, all settings maxed)
|
| 186 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 187 |
|
| 188 |
-
def transcribe_with_whisper(wav_path, duration_secs):
|
| 189 |
"""
|
| 190 |
Runs Whisper medium with every accuracy setting maximized.
|
| 191 |
|
|
@@ -232,20 +232,51 @@ def transcribe_with_whisper(wav_path, duration_secs):
|
|
| 232 |
print(f"[INFO] ESTIMATED TIME : ~{est_time}s")
|
| 233 |
print(f"[INFO] βββββββββββββββββββββββββββββββββββββββββ")
|
| 234 |
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
# Build clean transcript from segments
|
| 251 |
segments = result.get("segments", [])
|
|
@@ -259,8 +290,30 @@ def transcribe_with_whisper(wav_path, duration_secs):
|
|
| 259 |
full_text = " ".join(lines).strip()
|
| 260 |
word_count = len(full_text.split())
|
| 261 |
|
|
|
|
|
|
|
| 262 |
print(f"[INFO] Transcription complete: {word_count} words from {len(segments)} segments")
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
|
| 265 |
|
| 266 |
# βββββββββββββββββββββββββββββββββββββββοΏ½οΏ½βββββββββββββββββββ
|
|
@@ -314,12 +367,15 @@ def _cleanup(paths):
|
|
| 314 |
# MAIN β called by app.py for every transcription request
|
| 315 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 316 |
|
| 317 |
-
def transcribe_audio(file_path):
|
| 318 |
"""
|
| 319 |
Full beast mode pipeline:
|
| 320 |
1. Preprocess (convert + normalize)
|
| 321 |
2. Vocal isolation via demucs (if available)
|
| 322 |
3. Whisper medium with max accuracy settings
|
|
|
|
|
|
|
|
|
|
| 323 |
"""
|
| 324 |
|
| 325 |
uploads_dir = os.path.dirname(file_path)
|
|
@@ -360,14 +416,19 @@ def transcribe_audio(file_path):
|
|
| 360 |
|
| 361 |
# ββ Step 3: Transcribe ββββββββββββββββββββββββ
|
| 362 |
if WHISPER_AVAILABLE:
|
| 363 |
-
|
| 364 |
-
|
|
|
|
|
|
|
|
|
|
| 365 |
if is_demucs_available():
|
| 366 |
engine += " + Demucs vocal isolation"
|
| 367 |
else:
|
| 368 |
print("[INFO] Whisper not installed β using Google fallback")
|
| 369 |
-
text
|
| 370 |
-
|
|
|
|
|
|
|
| 371 |
|
| 372 |
print("[INFO] === PIPELINE COMPLETE ===")
|
| 373 |
|
|
@@ -385,10 +446,46 @@ def transcribe_audio(file_path):
|
|
| 385 |
|
| 386 |
word_count = len(text.split())
|
| 387 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
return {
|
| 389 |
-
"success"
|
| 390 |
-
"transcript": text,
|
| 391 |
-
"duration"
|
| 392 |
-
"word_count": word_count,
|
| 393 |
-
"engine"
|
|
|
|
|
|
|
|
|
|
| 394 |
}
|
|
|
|
| 185 |
# STEP 3 β WHISPER TRANSCRIPTION (medium model, all settings maxed)
|
| 186 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 187 |
|
| 188 |
+
def transcribe_with_whisper(wav_path, duration_secs, mode="transcribe"):
|
| 189 |
"""
|
| 190 |
Runs Whisper medium with every accuracy setting maximized.
|
| 191 |
|
|
|
|
| 232 |
print(f"[INFO] ESTIMATED TIME : ~{est_time}s")
|
| 233 |
print(f"[INFO] βββββββββββββββββββββββββββββββββββββββββ")
|
| 234 |
|
| 235 |
+
# mode="translate_to_english" β Whisper auto-detects language, outputs English
|
| 236 |
+
# mode="transcribe" β Standard English transcription
|
| 237 |
+
whisper_task = "translate" if mode == "translate_to_english" else "transcribe"
|
| 238 |
+
whisper_lang = None if mode == "translate_to_english" else "en"
|
| 239 |
+
|
| 240 |
+
if mode == "translate_to_english":
|
| 241 |
+
print(f"[INFO] MODE: Multilingual β English translation")
|
| 242 |
+
print(f"[INFO] Whisper will auto-detect language and output English")
|
| 243 |
+
else:
|
| 244 |
+
print(f"[INFO] MODE: Standard English transcription")
|
| 245 |
+
|
| 246 |
+
# Translation mode needs different settings than transcription mode.
|
| 247 |
+
# The tensor size mismatch error happens because patience=2 and best_of=5
|
| 248 |
+
# conflict with Whisper's internal beam search in translation mode.
|
| 249 |
+
# Fix: use safer/simpler settings for translation, full settings for transcription.
|
| 250 |
+
if mode == "translate_to_english":
|
| 251 |
+
result = model.transcribe(
|
| 252 |
+
wav_path,
|
| 253 |
+
language = whisper_lang, # None = auto-detect
|
| 254 |
+
fp16 = False,
|
| 255 |
+
task = "translate", # translate any language β English
|
| 256 |
+
beam_size = 5,
|
| 257 |
+
temperature = 0,
|
| 258 |
+
condition_on_previous_text = True,
|
| 259 |
+
no_speech_threshold = 0.25,
|
| 260 |
+
compression_ratio_threshold= 2.6,
|
| 261 |
+
word_timestamps = True,
|
| 262 |
+
# NOTE: patience and best_of are intentionally excluded in translate mode
|
| 263 |
+
# They cause "Sizes of tensors must match" error with task="translate"
|
| 264 |
+
)
|
| 265 |
+
else:
|
| 266 |
+
result = model.transcribe(
|
| 267 |
+
wav_path,
|
| 268 |
+
language = "en",
|
| 269 |
+
fp16 = False,
|
| 270 |
+
task = "transcribe",
|
| 271 |
+
beam_size = 5,
|
| 272 |
+
best_of = 5,
|
| 273 |
+
temperature = 0,
|
| 274 |
+
patience = 2,
|
| 275 |
+
condition_on_previous_text = True,
|
| 276 |
+
no_speech_threshold = 0.25,
|
| 277 |
+
compression_ratio_threshold= 2.6,
|
| 278 |
+
word_timestamps = True,
|
| 279 |
+
)
|
| 280 |
|
| 281 |
# Build clean transcript from segments
|
| 282 |
segments = result.get("segments", [])
|
|
|
|
| 290 |
full_text = " ".join(lines).strip()
|
| 291 |
word_count = len(full_text.split())
|
| 292 |
|
| 293 |
+
# Extract detected language from Whisper result
|
| 294 |
+
detected_lang = result.get("language", "unknown")
|
| 295 |
print(f"[INFO] Transcription complete: {word_count} words from {len(segments)} segments")
|
| 296 |
+
print(f"[INFO] Detected language: {detected_lang}")
|
| 297 |
+
|
| 298 |
+
# Format segments with timestamps right here
|
| 299 |
+
# Each segment gets start_fmt and end_fmt so frontend can use immediately
|
| 300 |
+
formatted = []
|
| 301 |
+
for seg in segments:
|
| 302 |
+
start_sec = int(seg.get("start", 0))
|
| 303 |
+
end_sec = int(seg.get("end", 0))
|
| 304 |
+
seg_text = seg.get("text", "").strip()
|
| 305 |
+
if not seg_text:
|
| 306 |
+
continue
|
| 307 |
+
formatted.append({
|
| 308 |
+
"start" : start_sec,
|
| 309 |
+
"end" : end_sec,
|
| 310 |
+
"start_fmt": f"{start_sec // 60:02d}:{start_sec % 60:02d}",
|
| 311 |
+
"end_fmt" : f"{end_sec // 60:02d}:{end_sec % 60:02d}",
|
| 312 |
+
"text" : seg_text
|
| 313 |
+
})
|
| 314 |
+
|
| 315 |
+
print(f"[INFO] {len(formatted)} formatted segments ready for frontend")
|
| 316 |
+
return {"text": full_text, "detected_language": detected_lang, "segments": formatted}
|
| 317 |
|
| 318 |
|
| 319 |
# βββββββββββββββββββββββββββββββββββββββοΏ½οΏ½βββββββββββββββββββ
|
|
|
|
| 367 |
# MAIN β called by app.py for every transcription request
|
| 368 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 369 |
|
| 370 |
+
def transcribe_audio(file_path, mode="transcribe"):
|
| 371 |
"""
|
| 372 |
Full beast mode pipeline:
|
| 373 |
1. Preprocess (convert + normalize)
|
| 374 |
2. Vocal isolation via demucs (if available)
|
| 375 |
3. Whisper medium with max accuracy settings
|
| 376 |
+
|
| 377 |
+
mode = "transcribe" β standard English transcription
|
| 378 |
+
mode = "translate_to_english" β any language audio β English text
|
| 379 |
"""
|
| 380 |
|
| 381 |
uploads_dir = os.path.dirname(file_path)
|
|
|
|
| 416 |
|
| 417 |
# ββ Step 3: Transcribe ββββββββββββββββββββββββ
|
| 418 |
if WHISPER_AVAILABLE:
|
| 419 |
+
whisper_result = transcribe_with_whisper(audio_for_whisper, duration_secs, mode=mode)
|
| 420 |
+
text = whisper_result["text"]
|
| 421 |
+
detected_lang = whisper_result["detected_language"]
|
| 422 |
+
segments = whisper_result["segments"]
|
| 423 |
+
engine = f"Whisper {WHISPER_MODEL_SIZE}" + (" (MultilingualβEN)" if mode == "translate_to_english" else "")
|
| 424 |
if is_demucs_available():
|
| 425 |
engine += " + Demucs vocal isolation"
|
| 426 |
else:
|
| 427 |
print("[INFO] Whisper not installed β using Google fallback")
|
| 428 |
+
text = transcribe_with_google_fallback(audio_for_whisper, duration_ms)
|
| 429 |
+
detected_lang = "unknown"
|
| 430 |
+
segments = []
|
| 431 |
+
engine = "Google Speech Recognition"
|
| 432 |
|
| 433 |
print("[INFO] === PIPELINE COMPLETE ===")
|
| 434 |
|
|
|
|
| 446 |
|
| 447 |
word_count = len(text.split())
|
| 448 |
|
| 449 |
+
# Language code β full name mapping for display
|
| 450 |
+
lang_names = {
|
| 451 |
+
"en": "English", "hi": "Hindi", "de": "German", "fr": "French",
|
| 452 |
+
"es": "Spanish", "it": "Italian", "pt": "Portuguese", "ru": "Russian",
|
| 453 |
+
"ja": "Japanese", "ko": "Korean", "zh": "Chinese", "ar": "Arabic",
|
| 454 |
+
"nl": "Dutch", "pl": "Polish", "tr": "Turkish", "sv": "Swedish",
|
| 455 |
+
"da": "Danish", "fi": "Finnish", "nb": "Norwegian", "uk": "Ukrainian",
|
| 456 |
+
"cs": "Czech", "ro": "Romanian", "hu": "Hungarian", "el": "Greek",
|
| 457 |
+
"he": "Hebrew", "th": "Thai", "vi": "Vietnamese", "id": "Indonesian",
|
| 458 |
+
"ms": "Malay", "bn": "Bengali", "ur": "Urdu", "fa": "Persian",
|
| 459 |
+
"ta": "Tamil", "te": "Telugu", "ml": "Malayalam", "kn": "Kannada",
|
| 460 |
+
}
|
| 461 |
+
detected_lang_name = lang_names.get(detected_lang, detected_lang.upper() if detected_lang != "unknown" else "Unknown")
|
| 462 |
+
|
| 463 |
+
# Format segments here β convert raw Whisper segments to clean
|
| 464 |
+
# { start_fmt, end_fmt, text } objects for the frontend
|
| 465 |
+
formatted_segments = []
|
| 466 |
+
for seg in segments:
|
| 467 |
+
start_sec = int(seg.get("start", 0))
|
| 468 |
+
end_sec = int(seg.get("end", 0))
|
| 469 |
+
seg_text = seg.get("text", "").strip()
|
| 470 |
+
if not seg_text:
|
| 471 |
+
continue
|
| 472 |
+
formatted_segments.append({
|
| 473 |
+
"start" : start_sec,
|
| 474 |
+
"end" : end_sec,
|
| 475 |
+
"start_fmt": f"{start_sec // 60:02d}:{start_sec % 60:02d}",
|
| 476 |
+
"end_fmt" : f"{end_sec // 60:02d}:{end_sec % 60:02d}",
|
| 477 |
+
"text" : seg_text
|
| 478 |
+
})
|
| 479 |
+
|
| 480 |
+
print(f"[INFO] Returning {len(formatted_segments)} formatted segments to frontend")
|
| 481 |
+
|
| 482 |
return {
|
| 483 |
+
"success" : True,
|
| 484 |
+
"transcript" : text,
|
| 485 |
+
"duration" : round(duration_secs, 1),
|
| 486 |
+
"word_count" : word_count,
|
| 487 |
+
"engine" : engine,
|
| 488 |
+
"detected_language" : detected_lang,
|
| 489 |
+
"detected_language_name": detected_lang_name,
|
| 490 |
+
"segments" : formatted_segments
|
| 491 |
}
|
backend/translator.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# translator.py
|
| 2 |
+
# ---------------------------------------------------------
|
| 3 |
+
# THIS FILE HAS ONE JOB:
|
| 4 |
+
# Take a text + target language β return translated text
|
| 5 |
+
#
|
| 6 |
+
# We use deep-translator library which is:
|
| 7 |
+
# - 100% free, no API key needed
|
| 8 |
+
# - Supports 100+ languages
|
| 9 |
+
# - Uses Google Translate engine under the hood
|
| 10 |
+
# - Simple and reliable
|
| 11 |
+
# ---------------------------------------------------------
|
| 12 |
+
|
| 13 |
+
from deep_translator import GoogleTranslator
|
| 14 |
+
|
| 15 |
+
# All supported languages with their display names and codes
|
| 16 |
+
# Code is what Google Translate uses internally
|
| 17 |
+
SUPPORTED_LANGUAGES = {
|
| 18 |
+
"af": "Afrikaans",
|
| 19 |
+
"sq": "Albanian",
|
| 20 |
+
"ar": "Arabic",
|
| 21 |
+
"bn": "Bengali",
|
| 22 |
+
"bs": "Bosnian",
|
| 23 |
+
"bg": "Bulgarian",
|
| 24 |
+
"zh-CN": "Chinese (Simplified)",
|
| 25 |
+
"zh-TW": "Chinese (Traditional)",
|
| 26 |
+
"hr": "Croatian",
|
| 27 |
+
"cs": "Czech",
|
| 28 |
+
"da": "Danish",
|
| 29 |
+
"nl": "Dutch",
|
| 30 |
+
"en": "English",
|
| 31 |
+
"et": "Estonian",
|
| 32 |
+
"fi": "Finnish",
|
| 33 |
+
"fr": "French",
|
| 34 |
+
"de": "German",
|
| 35 |
+
"el": "Greek",
|
| 36 |
+
"gu": "Gujarati",
|
| 37 |
+
"hi": "Hindi",
|
| 38 |
+
"hu": "Hungarian",
|
| 39 |
+
"id": "Indonesian",
|
| 40 |
+
"it": "Italian",
|
| 41 |
+
"ja": "Japanese",
|
| 42 |
+
"kn": "Kannada",
|
| 43 |
+
"ko": "Korean",
|
| 44 |
+
"lv": "Latvian",
|
| 45 |
+
"lt": "Lithuanian",
|
| 46 |
+
"ms": "Malay",
|
| 47 |
+
"ml": "Malayalam",
|
| 48 |
+
"mr": "Marathi",
|
| 49 |
+
"ne": "Nepali",
|
| 50 |
+
"no": "Norwegian",
|
| 51 |
+
"fa": "Persian",
|
| 52 |
+
"pl": "Polish",
|
| 53 |
+
"pt": "Portuguese",
|
| 54 |
+
"pa": "Punjabi",
|
| 55 |
+
"ro": "Romanian",
|
| 56 |
+
"ru": "Russian",
|
| 57 |
+
"sr": "Serbian",
|
| 58 |
+
"si": "Sinhala",
|
| 59 |
+
"sk": "Slovak",
|
| 60 |
+
"sl": "Slovenian",
|
| 61 |
+
"es": "Spanish",
|
| 62 |
+
"sw": "Swahili",
|
| 63 |
+
"sv": "Swedish",
|
| 64 |
+
"tl": "Filipino",
|
| 65 |
+
"ta": "Tamil",
|
| 66 |
+
"te": "Telugu",
|
| 67 |
+
"th": "Thai",
|
| 68 |
+
"tr": "Turkish",
|
| 69 |
+
"uk": "Ukrainian",
|
| 70 |
+
"ur": "Urdu",
|
| 71 |
+
"vi": "Vietnamese",
|
| 72 |
+
"cy": "Welsh",
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def translate_text(text, target_language_code):
|
| 77 |
+
"""
|
| 78 |
+
Translates text to the target language.
|
| 79 |
+
|
| 80 |
+
text β the transcript string to translate
|
| 81 |
+
target_language_code β e.g. "hi" for Hindi, "fr" for French
|
| 82 |
+
|
| 83 |
+
Returns a dict with success status and translated text.
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
# Validate the language code
|
| 87 |
+
if target_language_code not in SUPPORTED_LANGUAGES:
|
| 88 |
+
return {
|
| 89 |
+
"success": False,
|
| 90 |
+
"error": f"Unsupported language code: {target_language_code}"
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
try:
|
| 94 |
+
print(f"[INFO] Translating to {SUPPORTED_LANGUAGES[target_language_code]}...")
|
| 95 |
+
|
| 96 |
+
# GoogleTranslator takes source and target language
|
| 97 |
+
# source="auto" means it detects the input language automatically
|
| 98 |
+
translator = GoogleTranslator(
|
| 99 |
+
source="auto",
|
| 100 |
+
target=target_language_code
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
# Google Translate has a ~5000 character limit per request
|
| 104 |
+
# For long transcripts we split into chunks and translate each
|
| 105 |
+
if len(text) <= 4500:
|
| 106 |
+
translated = translator.translate(text)
|
| 107 |
+
else:
|
| 108 |
+
translated = translate_long_text(text, target_language_code)
|
| 109 |
+
|
| 110 |
+
print(f"[INFO] Translation complete!")
|
| 111 |
+
|
| 112 |
+
return {
|
| 113 |
+
"success": True,
|
| 114 |
+
"translated": translated,
|
| 115 |
+
"language": SUPPORTED_LANGUAGES[target_language_code],
|
| 116 |
+
"language_code": target_language_code
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
except Exception as e:
|
| 120 |
+
return {
|
| 121 |
+
"success": False,
|
| 122 |
+
"error": f"Translation failed: {str(e)}"
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def translate_long_text(text, target_language_code):
|
| 127 |
+
"""
|
| 128 |
+
Splits long text into chunks of ~4500 chars,
|
| 129 |
+
translates each chunk, then joins them back together.
|
| 130 |
+
"""
|
| 131 |
+
# Split by sentences (periods) to avoid cutting mid-sentence
|
| 132 |
+
sentences = text.replace(". ", ".|").split("|")
|
| 133 |
+
chunks = []
|
| 134 |
+
current_chunk = ""
|
| 135 |
+
|
| 136 |
+
for sentence in sentences:
|
| 137 |
+
if len(current_chunk) + len(sentence) < 4500:
|
| 138 |
+
current_chunk += sentence + " "
|
| 139 |
+
else:
|
| 140 |
+
chunks.append(current_chunk.strip())
|
| 141 |
+
current_chunk = sentence + " "
|
| 142 |
+
|
| 143 |
+
if current_chunk:
|
| 144 |
+
chunks.append(current_chunk.strip())
|
| 145 |
+
|
| 146 |
+
# Translate each chunk
|
| 147 |
+
translator = GoogleTranslator(source="auto", target=target_language_code)
|
| 148 |
+
translated_chunks = []
|
| 149 |
+
|
| 150 |
+
for i, chunk in enumerate(chunks):
|
| 151 |
+
print(f"[INFO] Translating chunk {i+1}/{len(chunks)}...")
|
| 152 |
+
translated_chunks.append(translator.translate(chunk))
|
| 153 |
+
|
| 154 |
+
return " ".join(translated_chunks)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def get_supported_languages():
|
| 158 |
+
"""Returns the full list of supported languages for the frontend."""
|
| 159 |
+
return SUPPORTED_LANGUAGES
|
frontend/app.js
CHANGED
|
@@ -1,30 +1,71 @@
|
|
| 1 |
/*
|
| 2 |
-
app.js
|
| 3 |
-
|
| 4 |
-
UPDATED FOR HUGGING FACE DEPLOYMENT:
|
| 5 |
-
|
| 6 |
-
When running locally: API_URL = "http://localhost:7860"
|
| 7 |
-
When running on HF: API_URL = "" (empty = same server)
|
| 8 |
-
|
| 9 |
-
Because on HF Spaces, Flask serves BOTH the frontend HTML
|
| 10 |
-
AND the /transcribe API from the same server. So we use
|
| 11 |
-
a relative URL β no need to hardcode any domain.
|
| 12 |
-
---------------------------------------------------------
|
| 13 |
*/
|
| 14 |
|
| 15 |
-
// Empty string "" means "same server this page is served from"
|
| 16 |
-
// Works both locally (localhost:7860) and on HF Spaces URL
|
| 17 |
const API_URL = "";
|
| 18 |
|
| 19 |
-
// ββ
|
| 20 |
-
let mediaRecorder
|
| 21 |
-
let audioChunks
|
| 22 |
-
let timerInterval
|
| 23 |
-
let secondsElapsed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
|
| 26 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 27 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 29 |
|
| 30 |
document.getElementById("audio-file-input").addEventListener("change", function () {
|
|
@@ -37,7 +78,7 @@ document.getElementById("audio-file-input").addEventListener("change", function
|
|
| 37 |
|
| 38 |
async function uploadAndTranscribe() {
|
| 39 |
const fileInput = document.getElementById("audio-file-input");
|
| 40 |
-
const file
|
| 41 |
|
| 42 |
if (!file) {
|
| 43 |
alert("Please select an audio file first!");
|
|
@@ -46,18 +87,17 @@ async function uploadAndTranscribe() {
|
|
| 46 |
|
| 47 |
const formData = new FormData();
|
| 48 |
formData.append("audio", file);
|
|
|
|
| 49 |
|
| 50 |
showLoading();
|
| 51 |
|
| 52 |
try {
|
| 53 |
const response = await fetch(`${API_URL}/transcribe`, {
|
| 54 |
method: "POST",
|
| 55 |
-
body:
|
| 56 |
});
|
| 57 |
-
|
| 58 |
const result = await response.json();
|
| 59 |
handleResult(result);
|
| 60 |
-
|
| 61 |
} catch (error) {
|
| 62 |
showError("Could not connect to backend. Is the server running? (python backend/app.py)");
|
| 63 |
}
|
|
@@ -65,19 +105,15 @@ async function uploadAndTranscribe() {
|
|
| 65 |
|
| 66 |
|
| 67 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 68 |
-
//
|
| 69 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 70 |
|
| 71 |
async function startRecording() {
|
| 72 |
try {
|
| 73 |
-
const stream
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
: "audio/ogg";
|
| 78 |
-
mediaRecorder = new MediaRecorder(stream, { mimeType });
|
| 79 |
-
|
| 80 |
-
audioChunks = [];
|
| 81 |
|
| 82 |
mediaRecorder.ondataavailable = (event) => {
|
| 83 |
if (event.data.size > 0) audioChunks.push(event.data);
|
|
@@ -90,12 +126,10 @@ async function startRecording() {
|
|
| 90 |
};
|
| 91 |
|
| 92 |
mediaRecorder.start();
|
| 93 |
-
|
| 94 |
document.body.classList.add("recording");
|
| 95 |
document.getElementById("record-status").textContent = "Recording...";
|
| 96 |
document.getElementById("start-btn").disabled = true;
|
| 97 |
-
document.getElementById("stop-btn").disabled
|
| 98 |
-
|
| 99 |
startTimer();
|
| 100 |
|
| 101 |
} catch (error) {
|
|
@@ -107,25 +141,24 @@ function stopRecording() {
|
|
| 107 |
if (mediaRecorder && mediaRecorder.state !== "inactive") {
|
| 108 |
mediaRecorder.stop();
|
| 109 |
}
|
| 110 |
-
|
| 111 |
document.body.classList.remove("recording");
|
| 112 |
document.getElementById("record-status").textContent = "Processing...";
|
| 113 |
document.getElementById("start-btn").disabled = false;
|
| 114 |
-
document.getElementById("stop-btn").disabled
|
| 115 |
-
|
| 116 |
stopTimer();
|
| 117 |
showLoading();
|
| 118 |
}
|
| 119 |
|
| 120 |
async function sendAudioToBackend(audioBlob) {
|
| 121 |
const formData = new FormData();
|
| 122 |
-
const ext
|
| 123 |
formData.append("audio", audioBlob, `recording.${ext}`);
|
|
|
|
| 124 |
|
| 125 |
try {
|
| 126 |
const response = await fetch(`${API_URL}/transcribe`, {
|
| 127 |
method: "POST",
|
| 128 |
-
body:
|
| 129 |
});
|
| 130 |
const result = await response.json();
|
| 131 |
handleResult(result);
|
|
@@ -138,72 +171,99 @@ async function sendAudioToBackend(audioBlob) {
|
|
| 138 |
|
| 139 |
|
| 140 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 141 |
-
//
|
| 142 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 143 |
|
| 144 |
function handleResult(result) {
|
| 145 |
hideLoading();
|
| 146 |
if (result.success) {
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
} else {
|
| 149 |
showError(result.error);
|
| 150 |
}
|
| 151 |
}
|
| 152 |
|
| 153 |
-
function showTranscript(text, duration,
|
|
|
|
| 154 |
document.getElementById("results-section").classList.add("visible");
|
| 155 |
document.getElementById("result-box").classList.add("visible");
|
| 156 |
document.getElementById("error-box").classList.remove("visible");
|
|
|
|
|
|
|
| 157 |
document.getElementById("transcript-text").textContent = text;
|
|
|
|
| 158 |
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
if (duration) meta += ` Β· ${duration}s audio`;
|
| 162 |
-
if (chunks && chunks > 1) meta += ` Β· ${chunks} chunks processed`;
|
| 163 |
document.getElementById("word-count").textContent = meta;
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
document.getElementById("results-section").scrollIntoView({ behavior: "smooth" });
|
| 166 |
}
|
| 167 |
|
| 168 |
function showError(message) {
|
| 169 |
document.getElementById("results-section").classList.add("visible");
|
| 170 |
document.getElementById("result-box").classList.remove("visible");
|
| 171 |
-
const errorBox = document.getElementById("error-box");
|
| 172 |
document.getElementById("error-text").textContent = message;
|
| 173 |
-
|
| 174 |
document.getElementById("results-section").scrollIntoView({ behavior: "smooth" });
|
| 175 |
}
|
| 176 |
|
| 177 |
-
// Rotating messages so user knows pipeline is actively working
|
| 178 |
-
const loadingMessages = [
|
| 179 |
-
'Starting beast mode pipeline...',
|
| 180 |
-
'Converting audio format...',
|
| 181 |
-
'Demucs isolating vocals from background...',
|
| 182 |
-
'Whisper AI transcribing clean vocals...',
|
| 183 |
-
'Almost there β processing final segments...',
|
| 184 |
-
'Finalizing transcript...'
|
| 185 |
-
];
|
| 186 |
-
let loadingMsgInterval = null;
|
| 187 |
-
|
| 188 |
function showLoading() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
let msgIndex = 0;
|
| 190 |
-
const msgEl
|
| 191 |
if (msgEl) msgEl.textContent = loadingMessages[0];
|
| 192 |
loadingMsgInterval = setInterval(() => {
|
| 193 |
msgIndex = (msgIndex + 1) % loadingMessages.length;
|
| 194 |
if (msgEl) msgEl.textContent = loadingMessages[msgIndex];
|
| 195 |
}, 4000);
|
| 196 |
-
|
| 197 |
-
document.getElementById("results-section").classList.add("visible");
|
| 198 |
-
document.getElementById("loading-state").classList.add("visible");
|
| 199 |
-
document.getElementById("result-box").classList.remove("visible");
|
| 200 |
-
document.getElementById("error-box").classList.remove("visible");
|
| 201 |
}
|
| 202 |
|
| 203 |
function hideLoading() {
|
| 204 |
-
if (loadingMsgInterval) { clearInterval(loadingMsgInterval); loadingMsgInterval = null; }
|
| 205 |
-
|
| 206 |
document.getElementById("loading-state").classList.remove("visible");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
}
|
| 208 |
|
| 209 |
function clearResults() {
|
|
@@ -212,13 +272,206 @@ function clearResults() {
|
|
| 212 |
document.getElementById("error-box").classList.remove("visible");
|
| 213 |
document.getElementById("loading-state").classList.remove("visible");
|
| 214 |
document.getElementById("transcript-text").textContent = "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
}
|
| 216 |
|
| 217 |
|
| 218 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 219 |
-
//
|
| 220 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
function copyTranscript() {
|
| 223 |
const text = document.getElementById("transcript-text").textContent;
|
| 224 |
navigator.clipboard.writeText(text).then(() => {
|
|
@@ -230,7 +483,7 @@ function copyTranscript() {
|
|
| 230 |
|
| 231 |
function startTimer() {
|
| 232 |
secondsElapsed = 0;
|
| 233 |
-
timerInterval
|
| 234 |
secondsElapsed++;
|
| 235 |
const mins = String(Math.floor(secondsElapsed / 60)).padStart(2, "0");
|
| 236 |
const secs = String(secondsElapsed % 60).padStart(2, "0");
|
|
@@ -244,26 +497,23 @@ function stopTimer() {
|
|
| 244 |
secondsElapsed = 0;
|
| 245 |
}
|
| 246 |
|
| 247 |
-
// Drag and drop
|
| 248 |
const dropZone = document.getElementById("drop-zone");
|
| 249 |
-
|
| 250 |
dropZone.addEventListener("dragover", (e) => {
|
| 251 |
e.preventDefault();
|
| 252 |
dropZone.style.borderColor = "#6366f1";
|
| 253 |
});
|
| 254 |
-
|
| 255 |
dropZone.addEventListener("dragleave", () => {
|
| 256 |
dropZone.style.borderColor = "";
|
| 257 |
});
|
| 258 |
-
|
| 259 |
dropZone.addEventListener("drop", (e) => {
|
| 260 |
e.preventDefault();
|
| 261 |
dropZone.style.borderColor = "";
|
| 262 |
const file = e.dataTransfer.files[0];
|
| 263 |
if (file) {
|
| 264 |
-
const
|
| 265 |
-
|
| 266 |
-
document.getElementById("audio-file-input").files =
|
| 267 |
document.getElementById("file-name-display").textContent = file.name;
|
| 268 |
document.getElementById("drop-text").textContent = "File selected:";
|
| 269 |
}
|
|
|
|
| 1 |
/*
|
| 2 |
+
app.js β VoiceScript
|
| 3 |
+
Full frontend logic: upload, record, transcribe, translate, timestamps
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
*/
|
| 5 |
|
|
|
|
|
|
|
| 6 |
const API_URL = "";
|
| 7 |
|
| 8 |
+
// ββ Global state βββββββββββββββββββββββββββββββββββββββββββ
|
| 9 |
+
let mediaRecorder = null;
|
| 10 |
+
let audioChunks = [];
|
| 11 |
+
let timerInterval = null;
|
| 12 |
+
let secondsElapsed = 0;
|
| 13 |
+
let currentMode = "transcribe";
|
| 14 |
+
let lastTranscript = "";
|
| 15 |
+
let lastSegments = [];
|
| 16 |
+
let timestampsShowing = false;
|
| 17 |
+
|
| 18 |
+
// Loading messages that rotate while processing
|
| 19 |
+
const loadingMessages = [
|
| 20 |
+
"Starting beast mode pipeline...",
|
| 21 |
+
"Converting audio format...",
|
| 22 |
+
"Demucs isolating vocals from background...",
|
| 23 |
+
"Whisper AI transcribing clean vocals...",
|
| 24 |
+
"Almost there β processing final segments...",
|
| 25 |
+
"Finalizing transcript..."
|
| 26 |
+
];
|
| 27 |
+
let loadingMsgInterval = null;
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
+
// INIT β runs on page load
|
| 32 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
+
|
| 34 |
+
async function loadLanguages() {
|
| 35 |
+
try {
|
| 36 |
+
const response = await fetch(`${API_URL}/languages`);
|
| 37 |
+
const data = await response.json();
|
| 38 |
+
if (data.success) {
|
| 39 |
+
const select = document.getElementById("lang-select");
|
| 40 |
+
const sorted = Object.entries(data.languages).sort((a, b) => a[1].localeCompare(b[1]));
|
| 41 |
+
sorted.forEach(([code, name]) => {
|
| 42 |
+
const opt = document.createElement("option");
|
| 43 |
+
opt.value = code;
|
| 44 |
+
opt.textContent = name;
|
| 45 |
+
select.appendChild(opt);
|
| 46 |
+
});
|
| 47 |
+
}
|
| 48 |
+
} catch (e) {
|
| 49 |
+
console.log("Languages not loaded yet β server may be starting");
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
loadLanguages();
|
| 54 |
|
| 55 |
|
| 56 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 57 |
+
// MODE TOGGLE (Transcribe / Any Language β English)
|
| 58 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 59 |
+
|
| 60 |
+
function setMode(mode) {
|
| 61 |
+
currentMode = mode;
|
| 62 |
+
document.getElementById("mode-transcribe").classList.toggle("active", mode === "transcribe");
|
| 63 |
+
document.getElementById("mode-translate").classList.toggle("active", mode === "translate_to_english");
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 68 |
+
// FILE UPLOAD
|
| 69 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 70 |
|
| 71 |
document.getElementById("audio-file-input").addEventListener("change", function () {
|
|
|
|
| 78 |
|
| 79 |
async function uploadAndTranscribe() {
|
| 80 |
const fileInput = document.getElementById("audio-file-input");
|
| 81 |
+
const file = fileInput.files[0];
|
| 82 |
|
| 83 |
if (!file) {
|
| 84 |
alert("Please select an audio file first!");
|
|
|
|
| 87 |
|
| 88 |
const formData = new FormData();
|
| 89 |
formData.append("audio", file);
|
| 90 |
+
formData.append("mode", currentMode);
|
| 91 |
|
| 92 |
showLoading();
|
| 93 |
|
| 94 |
try {
|
| 95 |
const response = await fetch(`${API_URL}/transcribe`, {
|
| 96 |
method: "POST",
|
| 97 |
+
body: formData,
|
| 98 |
});
|
|
|
|
| 99 |
const result = await response.json();
|
| 100 |
handleResult(result);
|
|
|
|
| 101 |
} catch (error) {
|
| 102 |
showError("Could not connect to backend. Is the server running? (python backend/app.py)");
|
| 103 |
}
|
|
|
|
| 105 |
|
| 106 |
|
| 107 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 108 |
+
// MICROPHONE RECORDING
|
| 109 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 110 |
|
| 111 |
async function startRecording() {
|
| 112 |
try {
|
| 113 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 114 |
+
const mimeType = MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/ogg";
|
| 115 |
+
mediaRecorder = new MediaRecorder(stream, { mimeType });
|
| 116 |
+
audioChunks = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
mediaRecorder.ondataavailable = (event) => {
|
| 119 |
if (event.data.size > 0) audioChunks.push(event.data);
|
|
|
|
| 126 |
};
|
| 127 |
|
| 128 |
mediaRecorder.start();
|
|
|
|
| 129 |
document.body.classList.add("recording");
|
| 130 |
document.getElementById("record-status").textContent = "Recording...";
|
| 131 |
document.getElementById("start-btn").disabled = true;
|
| 132 |
+
document.getElementById("stop-btn").disabled = false;
|
|
|
|
| 133 |
startTimer();
|
| 134 |
|
| 135 |
} catch (error) {
|
|
|
|
| 141 |
if (mediaRecorder && mediaRecorder.state !== "inactive") {
|
| 142 |
mediaRecorder.stop();
|
| 143 |
}
|
|
|
|
| 144 |
document.body.classList.remove("recording");
|
| 145 |
document.getElementById("record-status").textContent = "Processing...";
|
| 146 |
document.getElementById("start-btn").disabled = false;
|
| 147 |
+
document.getElementById("stop-btn").disabled = true;
|
|
|
|
| 148 |
stopTimer();
|
| 149 |
showLoading();
|
| 150 |
}
|
| 151 |
|
| 152 |
async function sendAudioToBackend(audioBlob) {
|
| 153 |
const formData = new FormData();
|
| 154 |
+
const ext = audioBlob.type.includes("ogg") ? "ogg" : "webm";
|
| 155 |
formData.append("audio", audioBlob, `recording.${ext}`);
|
| 156 |
+
formData.append("mode", currentMode);
|
| 157 |
|
| 158 |
try {
|
| 159 |
const response = await fetch(`${API_URL}/transcribe`, {
|
| 160 |
method: "POST",
|
| 161 |
+
body: formData,
|
| 162 |
});
|
| 163 |
const result = await response.json();
|
| 164 |
handleResult(result);
|
|
|
|
| 171 |
|
| 172 |
|
| 173 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 174 |
+
// RESULT HANDLING
|
| 175 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 176 |
|
| 177 |
function handleResult(result) {
|
| 178 |
hideLoading();
|
| 179 |
if (result.success) {
|
| 180 |
+
lastSegments = result.segments || [];
|
| 181 |
+
showTranscript(
|
| 182 |
+
result.transcript,
|
| 183 |
+
result.duration,
|
| 184 |
+
result.word_count,
|
| 185 |
+
result.detected_language_name
|
| 186 |
+
);
|
| 187 |
} else {
|
| 188 |
showError(result.error);
|
| 189 |
}
|
| 190 |
}
|
| 191 |
|
| 192 |
+
function showTranscript(text, duration, wordCount, detectedLang) {
|
| 193 |
+
// Show result section
|
| 194 |
document.getElementById("results-section").classList.add("visible");
|
| 195 |
document.getElementById("result-box").classList.add("visible");
|
| 196 |
document.getElementById("error-box").classList.remove("visible");
|
| 197 |
+
|
| 198 |
+
// Set plain transcript text
|
| 199 |
document.getElementById("transcript-text").textContent = text;
|
| 200 |
+
document.getElementById("transcript-text").style.display = "block";
|
| 201 |
|
| 202 |
+
// Reset timestamps view
|
| 203 |
+
document.getElementById("timestamps-view").style.display = "none";
|
| 204 |
+
timestampsShowing = false;
|
| 205 |
+
|
| 206 |
+
// Word count + duration meta
|
| 207 |
+
let meta = `${wordCount || text.trim().split(/\s+/).length} words`;
|
| 208 |
if (duration) meta += ` Β· ${duration}s audio`;
|
|
|
|
| 209 |
document.getElementById("word-count").textContent = meta;
|
| 210 |
|
| 211 |
+
// Language badge
|
| 212 |
+
const langBadge = document.getElementById("lang-badge");
|
| 213 |
+
if (detectedLang && detectedLang !== "Unknown") {
|
| 214 |
+
langBadge.textContent = "π " + detectedLang + " detected";
|
| 215 |
+
langBadge.style.display = "inline-block";
|
| 216 |
+
} else {
|
| 217 |
+
langBadge.style.display = "none";
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// Timestamps button β show only if we have segments
|
| 221 |
+
const tsBtn = document.getElementById("timestamps-btn");
|
| 222 |
+
if (lastSegments.length > 0) {
|
| 223 |
+
tsBtn.style.display = "inline-block";
|
| 224 |
+
tsBtn.textContent = "Show Timestamps";
|
| 225 |
+
tsBtn.classList.remove("active");
|
| 226 |
+
} else {
|
| 227 |
+
tsBtn.style.display = "none";
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
// Store for translation
|
| 231 |
+
lastTranscript = text;
|
| 232 |
+
document.getElementById("translated-box").classList.remove("visible");
|
| 233 |
+
document.getElementById("translate-panel").classList.add("visible");
|
| 234 |
+
|
| 235 |
document.getElementById("results-section").scrollIntoView({ behavior: "smooth" });
|
| 236 |
}
|
| 237 |
|
| 238 |
function showError(message) {
|
| 239 |
document.getElementById("results-section").classList.add("visible");
|
| 240 |
document.getElementById("result-box").classList.remove("visible");
|
|
|
|
| 241 |
document.getElementById("error-text").textContent = message;
|
| 242 |
+
document.getElementById("error-box").classList.add("visible");
|
| 243 |
document.getElementById("results-section").scrollIntoView({ behavior: "smooth" });
|
| 244 |
}
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
function showLoading() {
|
| 247 |
+
document.getElementById("results-section").classList.add("visible");
|
| 248 |
+
document.getElementById("loading-state").classList.add("visible");
|
| 249 |
+
document.getElementById("result-box").classList.remove("visible");
|
| 250 |
+
document.getElementById("error-box").classList.remove("visible");
|
| 251 |
+
|
| 252 |
let msgIndex = 0;
|
| 253 |
+
const msgEl = document.getElementById("loading-msg");
|
| 254 |
if (msgEl) msgEl.textContent = loadingMessages[0];
|
| 255 |
loadingMsgInterval = setInterval(() => {
|
| 256 |
msgIndex = (msgIndex + 1) % loadingMessages.length;
|
| 257 |
if (msgEl) msgEl.textContent = loadingMessages[msgIndex];
|
| 258 |
}, 4000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
}
|
| 260 |
|
| 261 |
function hideLoading() {
|
|
|
|
|
|
|
| 262 |
document.getElementById("loading-state").classList.remove("visible");
|
| 263 |
+
if (loadingMsgInterval) {
|
| 264 |
+
clearInterval(loadingMsgInterval);
|
| 265 |
+
loadingMsgInterval = null;
|
| 266 |
+
}
|
| 267 |
}
|
| 268 |
|
| 269 |
function clearResults() {
|
|
|
|
| 272 |
document.getElementById("error-box").classList.remove("visible");
|
| 273 |
document.getElementById("loading-state").classList.remove("visible");
|
| 274 |
document.getElementById("transcript-text").textContent = "";
|
| 275 |
+
document.getElementById("translate-panel").classList.remove("visible");
|
| 276 |
+
document.getElementById("translated-box").classList.remove("visible");
|
| 277 |
+
document.getElementById("timestamps-view").innerHTML = "";
|
| 278 |
+
lastTranscript = "";
|
| 279 |
+
lastSegments = [];
|
| 280 |
+
timestampsShowing = false;
|
| 281 |
}
|
| 282 |
|
| 283 |
|
| 284 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 285 |
+
// TIMESTAMPS
|
| 286 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 287 |
|
| 288 |
+
function toggleTimestamps() {
|
| 289 |
+
timestampsShowing = !timestampsShowing;
|
| 290 |
+
const btn = document.getElementById("timestamps-btn");
|
| 291 |
+
const plainView = document.getElementById("transcript-text");
|
| 292 |
+
const tsView = document.getElementById("timestamps-view");
|
| 293 |
+
|
| 294 |
+
if (timestampsShowing) {
|
| 295 |
+
// Build the timestamped lines
|
| 296 |
+
tsView.innerHTML = "";
|
| 297 |
+
lastSegments.forEach(seg => {
|
| 298 |
+
const line = document.createElement("div");
|
| 299 |
+
line.className = "ts-line";
|
| 300 |
+
line.innerHTML = `<span class="ts-badge">${seg.start_fmt}</span><span class="ts-text">${seg.text}</span>`;
|
| 301 |
+
tsView.appendChild(line);
|
| 302 |
+
});
|
| 303 |
+
|
| 304 |
+
plainView.style.display = "none";
|
| 305 |
+
tsView.style.display = "flex";
|
| 306 |
+
btn.textContent = "Hide Timestamps";
|
| 307 |
+
btn.classList.add("active");
|
| 308 |
+
} else {
|
| 309 |
+
plainView.style.display = "block";
|
| 310 |
+
tsView.style.display = "none";
|
| 311 |
+
btn.textContent = "Show Timestamps";
|
| 312 |
+
btn.classList.remove("active");
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 318 |
+
// TRANSLATION
|
| 319 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 320 |
+
|
| 321 |
+
async function translateTranscript() {
|
| 322 |
+
const langSelect = document.getElementById("lang-select");
|
| 323 |
+
const targetLang = langSelect.value;
|
| 324 |
+
|
| 325 |
+
if (!targetLang) {
|
| 326 |
+
alert("Please select a language first!");
|
| 327 |
+
return;
|
| 328 |
+
}
|
| 329 |
+
if (!lastTranscript) {
|
| 330 |
+
alert("No transcript to translate. Please transcribe audio first.");
|
| 331 |
+
return;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
const btn = document.getElementById("translate-btn");
|
| 335 |
+
btn.textContent = "Translating...";
|
| 336 |
+
btn.disabled = true;
|
| 337 |
+
|
| 338 |
+
try {
|
| 339 |
+
const response = await fetch(`${API_URL}/translate`, {
|
| 340 |
+
method: "POST",
|
| 341 |
+
headers: { "Content-Type": "application/json" },
|
| 342 |
+
body: JSON.stringify({ text: lastTranscript, target_language: targetLang })
|
| 343 |
+
});
|
| 344 |
+
const result = await response.json();
|
| 345 |
+
|
| 346 |
+
if (result.success) {
|
| 347 |
+
document.getElementById("translated-text").textContent = result.translated;
|
| 348 |
+
document.getElementById("translated-lang-label").textContent = "Translated to " + result.language;
|
| 349 |
+
document.getElementById("translated-box").classList.add("visible");
|
| 350 |
+
document.getElementById("translated-box").scrollIntoView({ behavior: "smooth" });
|
| 351 |
+
} else {
|
| 352 |
+
alert("Translation failed: " + result.error);
|
| 353 |
+
}
|
| 354 |
+
} catch (error) {
|
| 355 |
+
alert("Could not connect to translation service. Is the server running?");
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
btn.textContent = "Translate";
|
| 359 |
+
btn.disabled = false;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
function copyTranslation() {
|
| 363 |
+
const text = document.getElementById("translated-text").textContent;
|
| 364 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 365 |
+
const btns = document.querySelectorAll(".btn-copy");
|
| 366 |
+
if (btns[1]) {
|
| 367 |
+
btns[1].textContent = "Copied!";
|
| 368 |
+
setTimeout(() => btns[1].textContent = "Copy", 2000);
|
| 369 |
+
}
|
| 370 |
+
});
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 375 |
+
// UTILITIES
|
| 376 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 377 |
+
|
| 378 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 379 |
+
// EXPORT FUNCTIONS β TXT, SRT, PDF
|
| 380 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 381 |
+
|
| 382 |
+
function downloadFile(filename, content, mimeType) {
|
| 383 |
+
const blob = new Blob([content], { type: mimeType });
|
| 384 |
+
const url = URL.createObjectURL(blob);
|
| 385 |
+
const a = document.createElement("a");
|
| 386 |
+
a.href = url;
|
| 387 |
+
a.download = filename;
|
| 388 |
+
document.body.appendChild(a);
|
| 389 |
+
a.click();
|
| 390 |
+
document.body.removeChild(a);
|
| 391 |
+
URL.revokeObjectURL(url);
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
function exportTXT() {
|
| 395 |
+
if (!lastTranscript) { alert("No transcript to export!"); return; }
|
| 396 |
+
const now = new Date();
|
| 397 |
+
const dateStr = now.toLocaleDateString("en-IN", { day:"2-digit", month:"short", year:"numeric" });
|
| 398 |
+
const words = lastTranscript.trim().split(/\s+/).length;
|
| 399 |
+
const content = [
|
| 400 |
+
"VoiceScript β Transcript Export",
|
| 401 |
+
"================================",
|
| 402 |
+
"Date : " + dateStr,
|
| 403 |
+
"Words : " + words,
|
| 404 |
+
"",
|
| 405 |
+
"TRANSCRIPT",
|
| 406 |
+
"----------",
|
| 407 |
+
lastTranscript,
|
| 408 |
+
"",
|
| 409 |
+
"--------------------------------",
|
| 410 |
+
"Exported by VoiceScript Β· AI Speech Recognition",
|
| 411 |
+
"https://huggingface.co/spaces/Tusharz/VoiceScript"
|
| 412 |
+
].join("\n");
|
| 413 |
+
downloadFile("VoiceScript_Transcript.txt", content, "text/plain");
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
function secondsToSRT(totalSeconds) {
|
| 417 |
+
const h = Math.floor(totalSeconds / 3600);
|
| 418 |
+
const m = Math.floor((totalSeconds % 3600) / 60);
|
| 419 |
+
const s = Math.floor(totalSeconds % 60);
|
| 420 |
+
return String(h).padStart(2,"0") + ":" + String(m).padStart(2,"0") + ":" + String(s).padStart(2,"0") + ",000";
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
function exportSRT() {
|
| 424 |
+
if (!lastTranscript) { alert("No transcript to export!"); return; }
|
| 425 |
+
let srtContent = "";
|
| 426 |
+
|
| 427 |
+
if (lastSegments && lastSegments.length > 0) {
|
| 428 |
+
lastSegments.forEach(function(seg, i) {
|
| 429 |
+
srtContent += (i + 1) + "\n" + secondsToSRT(seg.start) + " --> " + secondsToSRT(seg.end) + "\n" + seg.text + "\n\n";
|
| 430 |
+
});
|
| 431 |
+
} else {
|
| 432 |
+
const words = lastTranscript.trim().split(/\s+/);
|
| 433 |
+
const size = 10;
|
| 434 |
+
let idx = 1;
|
| 435 |
+
for (let i = 0; i < words.length; i += size) {
|
| 436 |
+
const chunk = words.slice(i, i + size).join(" ");
|
| 437 |
+
srtContent += idx + "\n" + secondsToSRT((idx-1)*5) + " --> " + secondsToSRT(idx*5) + "\n" + chunk + "\n\n";
|
| 438 |
+
idx++;
|
| 439 |
+
}
|
| 440 |
+
}
|
| 441 |
+
downloadFile("VoiceScript_Subtitles.srt", srtContent, "text/plain");
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
function exportPDF() {
|
| 445 |
+
if (!lastTranscript) { alert("No transcript to export!"); return; }
|
| 446 |
+
const now = new Date();
|
| 447 |
+
const dateStr = now.toLocaleDateString("en-IN", { day:"2-digit", month:"long", year:"numeric" });
|
| 448 |
+
const words = lastTranscript.trim().split(/\s+/).length;
|
| 449 |
+
|
| 450 |
+
const printHTML = "<!DOCTYPE html><html><head><meta charset='UTF-8'/><title>VoiceScript Transcript</title>"
|
| 451 |
+
+ "<style>"
|
| 452 |
+
+ "body{font-family:Georgia,serif;color:#1a1a2e;padding:60px;max-width:800px;margin:0 auto;line-height:1.8}"
|
| 453 |
+
+ ".brand{font-family:Arial,sans-serif;font-size:13px;font-weight:700;color:#6366f1;letter-spacing:.1em;text-transform:uppercase;margin-bottom:8px}"
|
| 454 |
+
+ "h1{font-size:28px;color:#1a1a2e;margin-bottom:8px}"
|
| 455 |
+
+ ".header{border-bottom:3px solid #6366f1;padding-bottom:20px;margin-bottom:32px}"
|
| 456 |
+
+ ".meta{font-size:13px;color:#666;font-family:Arial,sans-serif}"
|
| 457 |
+
+ ".body{font-size:16px;line-height:2;text-align:justify;margin-bottom:40px}"
|
| 458 |
+
+ ".footer{border-top:1px solid #ddd;padding-top:16px;font-size:12px;color:#999;font-family:Arial,sans-serif;text-align:center}"
|
| 459 |
+
+ "</style></head><body>"
|
| 460 |
+
+ "<div class='header'><div class='brand'>VoiceScript β AI Speech Recognition</div>"
|
| 461 |
+
+ "<h1>Transcript</h1>"
|
| 462 |
+
+ "<div class='meta'>Generated on " + dateStr + " Β· " + words + " words</div></div>"
|
| 463 |
+
+ "<div class='body'>" + lastTranscript.replace(/\n/g, "<br/>") + "</div>"
|
| 464 |
+
+ "<div class='footer'>Transcribed by VoiceScript Β· Powered by OpenAI Whisper + Facebook Demucs<br/>"
|
| 465 |
+
+ "huggingface.co/spaces/Tusharz/VoiceScript</div>"
|
| 466 |
+
+ "</body></html>";
|
| 467 |
+
|
| 468 |
+
const win = window.open("", "_blank");
|
| 469 |
+
win.document.write(printHTML);
|
| 470 |
+
win.document.close();
|
| 471 |
+
win.focus();
|
| 472 |
+
setTimeout(function() { win.print(); }, 600);
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
function copyTranscript() {
|
| 476 |
const text = document.getElementById("transcript-text").textContent;
|
| 477 |
navigator.clipboard.writeText(text).then(() => {
|
|
|
|
| 483 |
|
| 484 |
function startTimer() {
|
| 485 |
secondsElapsed = 0;
|
| 486 |
+
timerInterval = setInterval(() => {
|
| 487 |
secondsElapsed++;
|
| 488 |
const mins = String(Math.floor(secondsElapsed / 60)).padStart(2, "0");
|
| 489 |
const secs = String(secondsElapsed % 60).padStart(2, "0");
|
|
|
|
| 497 |
secondsElapsed = 0;
|
| 498 |
}
|
| 499 |
|
| 500 |
+
// Drag and drop
|
| 501 |
const dropZone = document.getElementById("drop-zone");
|
|
|
|
| 502 |
dropZone.addEventListener("dragover", (e) => {
|
| 503 |
e.preventDefault();
|
| 504 |
dropZone.style.borderColor = "#6366f1";
|
| 505 |
});
|
|
|
|
| 506 |
dropZone.addEventListener("dragleave", () => {
|
| 507 |
dropZone.style.borderColor = "";
|
| 508 |
});
|
|
|
|
| 509 |
dropZone.addEventListener("drop", (e) => {
|
| 510 |
e.preventDefault();
|
| 511 |
dropZone.style.borderColor = "";
|
| 512 |
const file = e.dataTransfer.files[0];
|
| 513 |
if (file) {
|
| 514 |
+
const dt = new DataTransfer();
|
| 515 |
+
dt.items.add(file);
|
| 516 |
+
document.getElementById("audio-file-input").files = dt.files;
|
| 517 |
document.getElementById("file-name-display").textContent = file.name;
|
| 518 |
document.getElementById("drop-text").textContent = "File selected:";
|
| 519 |
}
|
frontend/index.html
CHANGED
|
@@ -30,33 +30,62 @@
|
|
| 30 |
<!-- ββ HEADER βββββββββββββββββββββββββββββββββββββββββ -->
|
| 31 |
<header class="header">
|
| 32 |
<div class="header-inner">
|
|
|
|
| 33 |
<div class="logo">
|
| 34 |
-
<
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
<
|
| 42 |
-
</
|
| 43 |
-
<span>VoiceScript</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
</div>
|
| 45 |
-
<div class="header-badge">AI Powered</div>
|
| 46 |
</div>
|
| 47 |
</header>
|
| 48 |
|
| 49 |
<!-- ββ MAIN CONTENT βββββββββββββββββββββββββββββββββββ -->
|
| 50 |
<main class="main">
|
| 51 |
|
| 52 |
-
<!-- Hero section
|
| 53 |
<section class="hero">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
<h1 class="hero-title">
|
| 55 |
-
Turn your voice<br/>into <span class="accent">text instantly</span>
|
| 56 |
</h1>
|
| 57 |
-
<p class="hero-sub">
|
| 58 |
Upload an audio file or record live β our AI transcribes it in seconds.
|
| 59 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
</section>
|
| 61 |
|
| 62 |
<!-- ββ TWO CARDS: Upload + Record βββββββββββββββββ -->
|
|
@@ -84,6 +113,16 @@
|
|
| 84 |
<span id="file-name-display" class="file-name-display"></span>
|
| 85 |
</div>
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
<!-- Button to send the file to backend -->
|
| 88 |
<button class="btn btn-primary" id="upload-btn" onclick="uploadAndTranscribe()">
|
| 89 |
Transcribe File
|
|
@@ -143,19 +182,96 @@
|
|
| 143 |
<div class="result-box" id="result-box">
|
| 144 |
<div class="result-header">
|
| 145 |
<h3>Transcript</h3>
|
| 146 |
-
<
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
</div>
|
| 151 |
-
|
|
|
|
| 152 |
<p class="transcript-text" id="transcript-text"></p>
|
| 153 |
|
|
|
|
|
|
|
|
|
|
| 154 |
<!-- Word count info -->
|
| 155 |
<div class="result-meta">
|
| 156 |
<span id="word-count"></span>
|
| 157 |
<button class="btn-clear" onclick="clearResults()">Clear</button>
|
| 158 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
</div>
|
| 160 |
|
| 161 |
<!-- Error box β shown if something goes wrong -->
|
|
@@ -169,24 +285,61 @@
|
|
| 169 |
|
| 170 |
<!-- ββ FOOTER ββββββββββββββββββββββββββββββββββββββββββ -->
|
| 171 |
<footer class="footer">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
</p>
|
| 187 |
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
</footer>
|
| 191 |
|
| 192 |
|
|
|
|
| 30 |
<!-- ββ HEADER βββββββββββββββββββββββββββββββββββββββββ -->
|
| 31 |
<header class="header">
|
| 32 |
<div class="header-inner">
|
| 33 |
+
<!-- Logo -->
|
| 34 |
<div class="logo">
|
| 35 |
+
<div class="logo-icon">
|
| 36 |
+
<svg width="20" height="20" viewBox="0 0 32 32" fill="none">
|
| 37 |
+
<rect x="6" y="12" width="3" height="8" rx="1.5" fill="white" opacity="0.7"/>
|
| 38 |
+
<rect x="11" y="8" width="3" height="16" rx="1.5" fill="white"/>
|
| 39 |
+
<rect x="16" y="5" width="3" height="22" rx="1.5" fill="white"/>
|
| 40 |
+
<rect x="21" y="9" width="3" height="14" rx="1.5" fill="white" opacity="0.9"/>
|
| 41 |
+
<rect x="26" y="13" width="3" height="6" rx="1.5" fill="white" opacity="0.6"/>
|
| 42 |
+
</svg>
|
| 43 |
+
</div>
|
| 44 |
+
<span class="logo-text">VoiceScript</span>
|
| 45 |
+
<span class="logo-version">v2.0</span>
|
| 46 |
+
</div>
|
| 47 |
+
|
| 48 |
+
<!-- Nav right -->
|
| 49 |
+
<div class="header-right">
|
| 50 |
+
<div class="header-status">
|
| 51 |
+
<span class="status-dot"></span>
|
| 52 |
+
<span class="status-text">Model ready</span>
|
| 53 |
+
</div>
|
| 54 |
+
<a href="https://github.com/TUSHARTAMRAKAR/VoiceScript" target="_blank" class="header-link">
|
| 55 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/></svg>
|
| 56 |
+
GitHub
|
| 57 |
+
</a>
|
| 58 |
+
<a href="https://huggingface.co/spaces/Tusharz/VoiceScript" target="_blank" class="header-btn-live">
|
| 59 |
+
π€ Live Demo
|
| 60 |
+
</a>
|
| 61 |
</div>
|
|
|
|
| 62 |
</div>
|
| 63 |
</header>
|
| 64 |
|
| 65 |
<!-- ββ MAIN CONTENT βββββββββββββββββββββββββββββββββββ -->
|
| 66 |
<main class="main">
|
| 67 |
|
| 68 |
+
<!-- Hero section -->
|
| 69 |
<section class="hero">
|
| 70 |
+
<div class="hero-eyebrow">
|
| 71 |
+
<span class="eyebrow-dot"></span>
|
| 72 |
+
<span>Powered by OpenAI Whisper + Facebook Demucs</span>
|
| 73 |
+
</div>
|
| 74 |
<h1 class="hero-title">
|
| 75 |
+
Turn your voice<br/>into <span class="accent" id="hero-accent">text instantly</span>
|
| 76 |
</h1>
|
| 77 |
+
<p class="hero-sub" id="hero-sub">
|
| 78 |
Upload an audio file or record live β our AI transcribes it in seconds.
|
| 79 |
</p>
|
| 80 |
+
<div class="hero-stats">
|
| 81 |
+
<div class="stat"><span class="stat-num">99+</span><span class="stat-label">Languages</span></div>
|
| 82 |
+
<div class="stat-divider"></div>
|
| 83 |
+
<div class="stat"><span class="stat-num">β</span><span class="stat-label">Audio Length</span></div>
|
| 84 |
+
<div class="stat-divider"></div>
|
| 85 |
+
<div class="stat"><span class="stat-num">100%</span><span class="stat-label">Private</span></div>
|
| 86 |
+
<div class="stat-divider"></div>
|
| 87 |
+
<div class="stat"><span class="stat-num">Free</span><span class="stat-label">Forever</span></div>
|
| 88 |
+
</div>
|
| 89 |
</section>
|
| 90 |
|
| 91 |
<!-- ββ TWO CARDS: Upload + Record βββββββββββββββββ -->
|
|
|
|
| 113 |
<span id="file-name-display" class="file-name-display"></span>
|
| 114 |
</div>
|
| 115 |
|
| 116 |
+
<!-- Mode toggle: Transcribe vs Translate to English -->
|
| 117 |
+
<div class="mode-toggle" id="mode-toggle">
|
| 118 |
+
<button class="mode-btn active" id="mode-transcribe" onclick="setMode('transcribe')">
|
| 119 |
+
Transcribe
|
| 120 |
+
</button>
|
| 121 |
+
<button class="mode-btn" id="mode-translate" onclick="setMode('translate_to_english')">
|
| 122 |
+
Any Language β English
|
| 123 |
+
</button>
|
| 124 |
+
</div>
|
| 125 |
+
|
| 126 |
<!-- Button to send the file to backend -->
|
| 127 |
<button class="btn btn-primary" id="upload-btn" onclick="uploadAndTranscribe()">
|
| 128 |
Transcribe File
|
|
|
|
| 182 |
<div class="result-box" id="result-box">
|
| 183 |
<div class="result-header">
|
| 184 |
<h3>Transcript</h3>
|
| 185 |
+
<div class="result-header-right">
|
| 186 |
+
<!-- Detected language badge -->
|
| 187 |
+
<span class="lang-badge" id="lang-badge" style="display:none;"></span>
|
| 188 |
+
<!-- Timestamps toggle -->
|
| 189 |
+
<button class="btn-timestamps" id="timestamps-btn" onclick="toggleTimestamps()" style="display:none;">
|
| 190 |
+
Show Timestamps
|
| 191 |
+
</button>
|
| 192 |
+
<!-- Copy to clipboard button -->
|
| 193 |
+
<button class="btn-copy" id="copy-btn" onclick="copyTranscript()">
|
| 194 |
+
Copy Text
|
| 195 |
+
</button>
|
| 196 |
+
</div>
|
| 197 |
</div>
|
| 198 |
+
|
| 199 |
+
<!-- Plain transcript view (default) -->
|
| 200 |
<p class="transcript-text" id="transcript-text"></p>
|
| 201 |
|
| 202 |
+
<!-- Timestamped transcript view (shown when timestamps toggled on) -->
|
| 203 |
+
<div class="timestamps-view" id="timestamps-view" style="display:none;"></div>
|
| 204 |
+
|
| 205 |
<!-- Word count info -->
|
| 206 |
<div class="result-meta">
|
| 207 |
<span id="word-count"></span>
|
| 208 |
<button class="btn-clear" onclick="clearResults()">Clear</button>
|
| 209 |
</div>
|
| 210 |
+
|
| 211 |
+
<!-- Export section -->
|
| 212 |
+
<div class="export-section">
|
| 213 |
+
<div class="export-header">
|
| 214 |
+
<span class="export-label">β¬ Download Transcript</span>
|
| 215 |
+
<span class="export-hint">Choose your format</span>
|
| 216 |
+
</div>
|
| 217 |
+
<div class="export-btns">
|
| 218 |
+
|
| 219 |
+
<button class="btn-export btn-export-txt" onclick="exportTXT()">
|
| 220 |
+
<div class="export-icon">
|
| 221 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
| 222 |
+
</div>
|
| 223 |
+
<div class="export-info">
|
| 224 |
+
<span class="export-name">Plain Text</span>
|
| 225 |
+
<span class="export-ext">.txt</span>
|
| 226 |
+
</div>
|
| 227 |
+
</button>
|
| 228 |
+
|
| 229 |
+
<button class="btn-export btn-export-srt" onclick="exportSRT()">
|
| 230 |
+
<div class="export-icon">
|
| 231 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="20" rx="3"/><path d="M7 8h10M7 12h10M7 16h6"/></svg>
|
| 232 |
+
</div>
|
| 233 |
+
<div class="export-info">
|
| 234 |
+
<span class="export-name">Subtitles</span>
|
| 235 |
+
<span class="export-ext">.srt</span>
|
| 236 |
+
</div>
|
| 237 |
+
</button>
|
| 238 |
+
|
| 239 |
+
<button class="btn-export btn-export-pdf" onclick="exportPDF()">
|
| 240 |
+
<div class="export-icon">
|
| 241 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="M9 13h1a1 1 0 010 2H9v-3m5 0h1.5a1.5 1.5 0 010 3H14v-3m4 0v4"/></svg>
|
| 242 |
+
</div>
|
| 243 |
+
<div class="export-info">
|
| 244 |
+
<span class="export-name">Document</span>
|
| 245 |
+
<span class="export-ext">.pdf</span>
|
| 246 |
+
</div>
|
| 247 |
+
</button>
|
| 248 |
+
|
| 249 |
+
</div>
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
|
| 253 |
+
<!-- Translate panel β shown after transcription -->
|
| 254 |
+
<div class="translate-panel" id="translate-panel">
|
| 255 |
+
<div class="translate-header">
|
| 256 |
+
<span class="translate-title">Translate Transcript</span>
|
| 257 |
+
<span class="translate-hint">Powered by Google Translate</span>
|
| 258 |
+
</div>
|
| 259 |
+
<div class="translate-controls">
|
| 260 |
+
<select class="lang-select" id="lang-select">
|
| 261 |
+
<option value="">Select language...</option>
|
| 262 |
+
</select>
|
| 263 |
+
<button class="btn btn-translate" id="translate-btn" onclick="translateTranscript()">
|
| 264 |
+
Translate
|
| 265 |
+
</button>
|
| 266 |
+
</div>
|
| 267 |
+
<!-- Translated result -->
|
| 268 |
+
<div class="translated-box" id="translated-box">
|
| 269 |
+
<div class="translated-header">
|
| 270 |
+
<span id="translated-lang-label">Translation</span>
|
| 271 |
+
<button class="btn-copy" onclick="copyTranslation()">Copy</button>
|
| 272 |
+
</div>
|
| 273 |
+
<p class="transcript-text" id="translated-text"></p>
|
| 274 |
+
</div>
|
| 275 |
</div>
|
| 276 |
|
| 277 |
<!-- Error box β shown if something goes wrong -->
|
|
|
|
| 285 |
|
| 286 |
<!-- ββ FOOTER ββββββββββββββββββββββββββββββββββββββββββ -->
|
| 287 |
<footer class="footer">
|
| 288 |
+
<div class="footer-inner">
|
| 289 |
+
|
| 290 |
+
<!-- Left: branding -->
|
| 291 |
+
<div class="footer-brand">
|
| 292 |
+
<div class="footer-logo">
|
| 293 |
+
<div class="logo-icon" style="width:28px;height:28px;">
|
| 294 |
+
<svg width="16" height="16" viewBox="0 0 32 32" fill="none">
|
| 295 |
+
<rect x="6" y="12" width="3" height="8" rx="1.5" fill="white" opacity="0.7"/>
|
| 296 |
+
<rect x="11" y="8" width="3" height="16" rx="1.5" fill="white"/>
|
| 297 |
+
<rect x="16" y="5" width="3" height="22" rx="1.5" fill="white"/>
|
| 298 |
+
<rect x="21" y="9" width="3" height="14" rx="1.5" fill="white" opacity="0.9"/>
|
| 299 |
+
<rect x="26" y="13" width="3" height="6" rx="1.5" fill="white" opacity="0.6"/>
|
| 300 |
+
</svg>
|
| 301 |
+
</div>
|
| 302 |
+
<span class="logo-text" style="font-size:16px;">VoiceScript</span>
|
| 303 |
+
</div>
|
| 304 |
+
<p class="footer-tagline">AI-powered speech recognition,<br/>built by Tushar Tamrakar.</p>
|
| 305 |
+
<p class="footer-by">Made with β€οΈ Β· <a href="https://github.com/TUSHARTAMRAKAR" target="_blank" class="footer-a">@TUSHARTAMRAKAR</a></p>
|
| 306 |
+
</div>
|
| 307 |
|
| 308 |
+
<!-- Center: stack -->
|
| 309 |
+
<div class="footer-center">
|
| 310 |
+
<p class="footer-stack-label">Built with</p>
|
| 311 |
+
<div class="footer-stack">
|
| 312 |
+
<span class="stack-badge badge-python">Python 3</span>
|
| 313 |
+
<span class="stack-badge badge-flask">Flask</span>
|
| 314 |
+
<span class="stack-badge badge-js">JavaScript</span>
|
| 315 |
+
<span class="stack-badge badge-whisper">Whisper AI</span>
|
| 316 |
+
<span class="stack-badge badge-demucs">Demucs</span>
|
| 317 |
+
<span class="stack-badge badge-pydub">pydub</span>
|
| 318 |
+
<span class="stack-badge badge-ffmpeg">ffmpeg</span>
|
| 319 |
+
</div>
|
| 320 |
+
</div>
|
|
|
|
| 321 |
|
| 322 |
+
<!-- Right: links -->
|
| 323 |
+
<div class="footer-links">
|
| 324 |
+
<p class="footer-stack-label">Links</p>
|
| 325 |
+
<a href="https://github.com/TUSHARTAMRAKAR/VoiceScript" target="_blank" class="footer-link-item">
|
| 326 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/></svg>
|
| 327 |
+
GitHub Repository
|
| 328 |
+
</a>
|
| 329 |
+
<a href="https://huggingface.co/spaces/Tusharz/VoiceScript" target="_blank" class="footer-link-item">
|
| 330 |
+
π€ Live on Hugging Face
|
| 331 |
+
</a>
|
| 332 |
+
<a href="mailto:tushartamrakar2003@gmail.com" class="footer-link-item">
|
| 333 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
|
| 334 |
+
Contact
|
| 335 |
+
</a>
|
| 336 |
+
</div>
|
| 337 |
|
| 338 |
+
</div>
|
| 339 |
+
<div class="footer-bottom">
|
| 340 |
+
<span>Β© 2026 VoiceScript Β· MIT License</span>
|
| 341 |
+
<span>Speech-to-text powered by OpenAI Whisper Β· Vocal isolation by Facebook Demucs</span>
|
| 342 |
+
</div>
|
| 343 |
</footer>
|
| 344 |
|
| 345 |
|
frontend/style.css
CHANGED
|
@@ -113,10 +113,27 @@ body {
|
|
| 113 |
margin-bottom: 16px;
|
| 114 |
}
|
| 115 |
|
| 116 |
-
/* The "text instantly" part β
|
| 117 |
.accent {
|
| 118 |
-
color: var(--accent-light);
|
| 119 |
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
}
|
| 121 |
|
| 122 |
.hero-sub {
|
|
@@ -469,3 +486,591 @@ body.recording .record-timer { color: var(--text-1); }
|
|
| 469 |
letter-spacing: 0.01em;
|
| 470 |
}
|
| 471 |
.footer-by .heart { color: #f472b6; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
margin-bottom: 16px;
|
| 114 |
}
|
| 115 |
|
| 116 |
+
/* The "text instantly" part β infinite shimmer animation */
|
| 117 |
.accent {
|
|
|
|
| 118 |
position: relative;
|
| 119 |
+
background: linear-gradient(
|
| 120 |
+
90deg,
|
| 121 |
+
#818cf8 0%,
|
| 122 |
+
#c084fc 25%,
|
| 123 |
+
#f472b6 50%,
|
| 124 |
+
#c084fc 75%,
|
| 125 |
+
#818cf8 100%
|
| 126 |
+
);
|
| 127 |
+
background-size: 200% auto;
|
| 128 |
+
-webkit-background-clip: text;
|
| 129 |
+
-webkit-text-fill-color: transparent;
|
| 130 |
+
background-clip: text;
|
| 131 |
+
animation: shimmer 3s linear infinite;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
@keyframes shimmer {
|
| 135 |
+
0% { background-position: 0% center; }
|
| 136 |
+
100% { background-position: 200% center; }
|
| 137 |
}
|
| 138 |
|
| 139 |
.hero-sub {
|
|
|
|
| 486 |
letter-spacing: 0.01em;
|
| 487 |
}
|
| 488 |
.footer-by .heart { color: #f472b6; }
|
| 489 |
+
|
| 490 |
+
/* ββ MODE TOGGLE ββββββββββββββββββββββββββββββββββββββββββ*/
|
| 491 |
+
.mode-toggle {
|
| 492 |
+
display: flex;
|
| 493 |
+
background: var(--bg-card-2);
|
| 494 |
+
border: 1px solid var(--border);
|
| 495 |
+
border-radius: 10px;
|
| 496 |
+
padding: 4px;
|
| 497 |
+
gap: 4px;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
.mode-btn {
|
| 501 |
+
flex: 1;
|
| 502 |
+
padding: 8px 10px;
|
| 503 |
+
border: none;
|
| 504 |
+
border-radius: 7px;
|
| 505 |
+
font-size: 12px;
|
| 506 |
+
font-weight: 500;
|
| 507 |
+
cursor: pointer;
|
| 508 |
+
background: transparent;
|
| 509 |
+
color: var(--text-3);
|
| 510 |
+
transition: all 0.2s;
|
| 511 |
+
font-family: 'Inter', sans-serif;
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
.mode-btn.active {
|
| 515 |
+
background: var(--accent);
|
| 516 |
+
color: white;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.mode-btn:hover:not(.active) {
|
| 520 |
+
color: var(--text-2);
|
| 521 |
+
background: rgba(255,255,255,0.05);
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
/* ββ TRANSLATE PANEL ββββββββββββββββββββββββββββββββββββββ*/
|
| 525 |
+
.translate-panel {
|
| 526 |
+
display: none;
|
| 527 |
+
background: var(--bg-card);
|
| 528 |
+
border: 1px solid var(--border);
|
| 529 |
+
border-radius: var(--radius);
|
| 530 |
+
padding: 24px;
|
| 531 |
+
margin-top: 16px;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
.translate-panel.visible { display: block; }
|
| 535 |
+
|
| 536 |
+
.translate-header {
|
| 537 |
+
display: flex;
|
| 538 |
+
justify-content: space-between;
|
| 539 |
+
align-items: center;
|
| 540 |
+
margin-bottom: 16px;
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
.translate-title {
|
| 544 |
+
font-family: 'Space Grotesk', sans-serif;
|
| 545 |
+
font-size: 16px;
|
| 546 |
+
font-weight: 600;
|
| 547 |
+
color: var(--text-1);
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
.translate-hint {
|
| 551 |
+
font-size: 11px;
|
| 552 |
+
color: var(--text-3);
|
| 553 |
+
background: var(--bg-card-2);
|
| 554 |
+
padding: 3px 10px;
|
| 555 |
+
border-radius: 20px;
|
| 556 |
+
border: 1px solid var(--border);
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
.translate-controls {
|
| 560 |
+
display: flex;
|
| 561 |
+
gap: 10px;
|
| 562 |
+
margin-bottom: 16px;
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.lang-select {
|
| 566 |
+
flex: 1;
|
| 567 |
+
padding: 10px 14px;
|
| 568 |
+
background: var(--bg-card-2);
|
| 569 |
+
border: 1px solid var(--border);
|
| 570 |
+
border-radius: 10px;
|
| 571 |
+
color: var(--text-1);
|
| 572 |
+
font-size: 14px;
|
| 573 |
+
font-family: 'Inter', sans-serif;
|
| 574 |
+
cursor: pointer;
|
| 575 |
+
outline: none;
|
| 576 |
+
transition: border-color 0.2s;
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
.lang-select:focus { border-color: var(--accent); }
|
| 580 |
+
.lang-select option { background: var(--bg-card); }
|
| 581 |
+
|
| 582 |
+
.btn-translate {
|
| 583 |
+
padding: 10px 20px;
|
| 584 |
+
background: linear-gradient(135deg, var(--accent), var(--accent-light));
|
| 585 |
+
color: white;
|
| 586 |
+
border: none;
|
| 587 |
+
border-radius: 10px;
|
| 588 |
+
font-size: 14px;
|
| 589 |
+
font-weight: 600;
|
| 590 |
+
cursor: pointer;
|
| 591 |
+
transition: opacity 0.2s, transform 0.2s;
|
| 592 |
+
font-family: 'Inter', sans-serif;
|
| 593 |
+
white-space: nowrap;
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
.btn-translate:hover { opacity: 0.9; transform: translateY(-1px); }
|
| 597 |
+
.btn-translate:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
|
| 598 |
+
|
| 599 |
+
.translated-box {
|
| 600 |
+
display: none;
|
| 601 |
+
background: var(--bg-card-2);
|
| 602 |
+
border: 1px solid rgba(99,102,241,0.25);
|
| 603 |
+
border-radius: 10px;
|
| 604 |
+
padding: 20px;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
.translated-box.visible { display: block; }
|
| 608 |
+
|
| 609 |
+
.translated-header {
|
| 610 |
+
display: flex;
|
| 611 |
+
justify-content: space-between;
|
| 612 |
+
align-items: center;
|
| 613 |
+
margin-bottom: 12px;
|
| 614 |
+
padding-bottom: 12px;
|
| 615 |
+
border-bottom: 1px solid var(--border);
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
#translated-lang-label {
|
| 619 |
+
font-size: 13px;
|
| 620 |
+
font-weight: 600;
|
| 621 |
+
color: var(--accent-light);
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
/* ββ LANGUAGE BADGE βββββββββββββββββββββββββββββββββββββββ*/
|
| 625 |
+
.result-header-right {
|
| 626 |
+
display: flex;
|
| 627 |
+
align-items: center;
|
| 628 |
+
gap: 10px;
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
.lang-badge {
|
| 632 |
+
font-size: 12px;
|
| 633 |
+
font-weight: 600;
|
| 634 |
+
padding: 4px 12px;
|
| 635 |
+
border-radius: 20px;
|
| 636 |
+
background: rgba(99,102,241,0.15);
|
| 637 |
+
color: var(--accent-light);
|
| 638 |
+
border: 1px solid rgba(99,102,241,0.3);
|
| 639 |
+
letter-spacing: 0.02em;
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
/* ββ TIMESTAMPS BUTTON ββββββββββββββββββββββββββββββββββββ*/
|
| 643 |
+
.btn-timestamps {
|
| 644 |
+
padding: 5px 12px;
|
| 645 |
+
border: 1px solid var(--border);
|
| 646 |
+
border-radius: 8px;
|
| 647 |
+
background: transparent;
|
| 648 |
+
color: var(--text-2);
|
| 649 |
+
font-size: 12px;
|
| 650 |
+
font-weight: 500;
|
| 651 |
+
cursor: pointer;
|
| 652 |
+
transition: all 0.2s;
|
| 653 |
+
font-family: 'Inter', sans-serif;
|
| 654 |
+
}
|
| 655 |
+
.btn-timestamps:hover { border-color: var(--accent); color: var(--accent-light); }
|
| 656 |
+
.btn-timestamps.active { background: var(--accent-glow); border-color: var(--accent); color: var(--accent-light); }
|
| 657 |
+
|
| 658 |
+
/* ββ TIMESTAMPS VIEW ββββββββββββββββββββββββοΏ½οΏ½βββββββββββββ*/
|
| 659 |
+
.timestamps-view {
|
| 660 |
+
display: flex;
|
| 661 |
+
flex-direction: column;
|
| 662 |
+
gap: 2px;
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
.ts-line {
|
| 666 |
+
display: flex;
|
| 667 |
+
gap: 12px;
|
| 668 |
+
align-items: flex-start;
|
| 669 |
+
padding: 8px 10px;
|
| 670 |
+
border-radius: 8px;
|
| 671 |
+
cursor: pointer;
|
| 672 |
+
transition: background 0.15s;
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.ts-line:hover { background: var(--bg-card-2); }
|
| 676 |
+
|
| 677 |
+
.ts-badge {
|
| 678 |
+
font-family: 'Space Grotesk', monospace;
|
| 679 |
+
font-size: 11px;
|
| 680 |
+
font-weight: 600;
|
| 681 |
+
color: var(--accent-light);
|
| 682 |
+
background: var(--accent-glow);
|
| 683 |
+
border: 1px solid rgba(99,102,241,0.2);
|
| 684 |
+
padding: 2px 8px;
|
| 685 |
+
border-radius: 6px;
|
| 686 |
+
white-space: nowrap;
|
| 687 |
+
flex-shrink: 0;
|
| 688 |
+
margin-top: 2px;
|
| 689 |
+
letter-spacing: 0.03em;
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
.ts-text {
|
| 693 |
+
font-size: 15px;
|
| 694 |
+
color: var(--text-1);
|
| 695 |
+
line-height: 1.6;
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
/* ββ TIMESTAMPS βββββββββββββββββββββββββββββββββββββββββββ*/
|
| 699 |
+
.btn-timestamps {
|
| 700 |
+
padding: 5px 12px;
|
| 701 |
+
border: 1px solid var(--border);
|
| 702 |
+
border-radius: 8px;
|
| 703 |
+
background: transparent;
|
| 704 |
+
color: var(--text-2);
|
| 705 |
+
font-size: 12px;
|
| 706 |
+
font-weight: 500;
|
| 707 |
+
cursor: pointer;
|
| 708 |
+
transition: all 0.2s;
|
| 709 |
+
font-family: 'Inter', sans-serif;
|
| 710 |
+
white-space: nowrap;
|
| 711 |
+
}
|
| 712 |
+
.btn-timestamps:hover { border-color: var(--accent); color: var(--accent-light); }
|
| 713 |
+
.btn-timestamps.active { background: var(--accent-glow); border-color: var(--accent); color: var(--accent-light); }
|
| 714 |
+
|
| 715 |
+
.timestamps-view {
|
| 716 |
+
display: flex;
|
| 717 |
+
flex-direction: column;
|
| 718 |
+
gap: 2px;
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
.ts-line {
|
| 722 |
+
display: flex;
|
| 723 |
+
gap: 14px;
|
| 724 |
+
align-items: flex-start;
|
| 725 |
+
padding: 8px 10px;
|
| 726 |
+
border-radius: 8px;
|
| 727 |
+
transition: background 0.15s;
|
| 728 |
+
cursor: default;
|
| 729 |
+
}
|
| 730 |
+
.ts-line:hover { background: var(--bg-card-2); }
|
| 731 |
+
|
| 732 |
+
.ts-badge {
|
| 733 |
+
font-family: 'Space Grotesk', monospace;
|
| 734 |
+
font-size: 11px;
|
| 735 |
+
font-weight: 600;
|
| 736 |
+
color: var(--accent-light);
|
| 737 |
+
background: var(--accent-glow);
|
| 738 |
+
border: 1px solid rgba(99,102,241,0.25);
|
| 739 |
+
border-radius: 6px;
|
| 740 |
+
padding: 3px 8px;
|
| 741 |
+
white-space: nowrap;
|
| 742 |
+
flex-shrink: 0;
|
| 743 |
+
margin-top: 2px;
|
| 744 |
+
min-width: 52px;
|
| 745 |
+
text-align: center;
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
.ts-text {
|
| 749 |
+
font-size: 15px;
|
| 750 |
+
color: var(--text-1);
|
| 751 |
+
line-height: 1.6;
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 755 |
+
PREMIUM UI UPGRADE
|
| 756 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 757 |
+
|
| 758 |
+
/* ββ HEADER UPGRADE βββββββββββββββββββββββββββββββββββββββ */
|
| 759 |
+
.header-right {
|
| 760 |
+
display: flex;
|
| 761 |
+
align-items: center;
|
| 762 |
+
gap: 12px;
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
.header-status {
|
| 766 |
+
display: flex;
|
| 767 |
+
align-items: center;
|
| 768 |
+
gap: 6px;
|
| 769 |
+
font-size: 12px;
|
| 770 |
+
color: var(--text-3);
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
.status-dot {
|
| 774 |
+
width: 7px;
|
| 775 |
+
height: 7px;
|
| 776 |
+
border-radius: 50%;
|
| 777 |
+
background: var(--success);
|
| 778 |
+
box-shadow: 0 0 6px var(--success);
|
| 779 |
+
animation: pulse-dot 2s ease-in-out infinite;
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
@keyframes pulse-dot {
|
| 783 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 784 |
+
50% { opacity: 0.6; transform: scale(0.85); }
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
.status-text { color: var(--text-3); font-size: 12px; }
|
| 788 |
+
|
| 789 |
+
.header-link {
|
| 790 |
+
display: flex;
|
| 791 |
+
align-items: center;
|
| 792 |
+
gap: 6px;
|
| 793 |
+
font-size: 13px;
|
| 794 |
+
color: var(--text-2);
|
| 795 |
+
text-decoration: none;
|
| 796 |
+
padding: 6px 12px;
|
| 797 |
+
border-radius: 8px;
|
| 798 |
+
border: 1px solid var(--border);
|
| 799 |
+
transition: all 0.2s;
|
| 800 |
+
}
|
| 801 |
+
.header-link:hover { color: var(--text-1); border-color: var(--text-3); }
|
| 802 |
+
|
| 803 |
+
.header-btn-live {
|
| 804 |
+
display: flex;
|
| 805 |
+
align-items: center;
|
| 806 |
+
gap: 6px;
|
| 807 |
+
font-size: 13px;
|
| 808 |
+
font-weight: 600;
|
| 809 |
+
color: white;
|
| 810 |
+
text-decoration: none;
|
| 811 |
+
padding: 7px 14px;
|
| 812 |
+
border-radius: 8px;
|
| 813 |
+
background: linear-gradient(135deg, #6366f1, #818cf8);
|
| 814 |
+
transition: all 0.2s;
|
| 815 |
+
box-shadow: 0 2px 12px rgba(99,102,241,0.3);
|
| 816 |
+
}
|
| 817 |
+
.header-btn-live:hover { transform: translateY(-1px); box-shadow: 0 4px 20px rgba(99,102,241,0.45); }
|
| 818 |
+
|
| 819 |
+
.logo-icon {
|
| 820 |
+
width: 34px;
|
| 821 |
+
height: 34px;
|
| 822 |
+
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
| 823 |
+
border-radius: 9px;
|
| 824 |
+
display: flex;
|
| 825 |
+
align-items: center;
|
| 826 |
+
justify-content: center;
|
| 827 |
+
box-shadow: 0 2px 10px rgba(99,102,241,0.35);
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
.logo-text { font-family: 'Space Grotesk', sans-serif; font-size: 20px; font-weight: 700; color: var(--text-1); }
|
| 831 |
+
.logo-version { font-size: 10px; font-weight: 600; color: var(--accent-light); background: var(--accent-glow); border: 1px solid rgba(99,102,241,0.3); padding: 1px 6px; border-radius: 20px; align-self: flex-start; margin-top: 4px; }
|
| 832 |
+
|
| 833 |
+
/* ββ HERO UPGRADE βββββββββββββββββββββββββββββββββββββββ */
|
| 834 |
+
.hero-eyebrow {
|
| 835 |
+
display: inline-flex;
|
| 836 |
+
align-items: center;
|
| 837 |
+
gap: 8px;
|
| 838 |
+
font-size: 12px;
|
| 839 |
+
font-weight: 500;
|
| 840 |
+
color: var(--accent-light);
|
| 841 |
+
background: var(--accent-glow);
|
| 842 |
+
border: 1px solid rgba(99,102,241,0.25);
|
| 843 |
+
padding: 6px 14px;
|
| 844 |
+
border-radius: 20px;
|
| 845 |
+
margin-bottom: 24px;
|
| 846 |
+
letter-spacing: 0.02em;
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
.eyebrow-dot {
|
| 850 |
+
width: 6px;
|
| 851 |
+
height: 6px;
|
| 852 |
+
background: var(--accent-light);
|
| 853 |
+
border-radius: 50%;
|
| 854 |
+
animation: pulse-dot 2s ease-in-out infinite;
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
/* Animated typewriter subtitle */
|
| 858 |
+
.hero-sub {
|
| 859 |
+
overflow: hidden;
|
| 860 |
+
border-right: 2px solid var(--accent-light);
|
| 861 |
+
white-space: nowrap;
|
| 862 |
+
animation: typing 3.5s steps(60, end), blink-caret 0.75s step-end infinite;
|
| 863 |
+
animation-fill-mode: forwards;
|
| 864 |
+
max-width: 520px;
|
| 865 |
+
margin: 0 auto 32px;
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
@keyframes typing {
|
| 869 |
+
from { width: 0; }
|
| 870 |
+
to { width: 100%; }
|
| 871 |
+
}
|
| 872 |
+
@keyframes blink-caret {
|
| 873 |
+
from, to { border-color: transparent; }
|
| 874 |
+
50% { border-color: var(--accent-light); }
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
/* Stats row */
|
| 878 |
+
.hero-stats {
|
| 879 |
+
display: flex;
|
| 880 |
+
align-items: center;
|
| 881 |
+
justify-content: center;
|
| 882 |
+
gap: 0;
|
| 883 |
+
margin-top: 8px;
|
| 884 |
+
background: var(--bg-card);
|
| 885 |
+
border: 1px solid var(--border);
|
| 886 |
+
border-radius: 14px;
|
| 887 |
+
padding: 16px 32px;
|
| 888 |
+
width: fit-content;
|
| 889 |
+
margin-left: auto;
|
| 890 |
+
margin-right: auto;
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
.stat {
|
| 894 |
+
display: flex;
|
| 895 |
+
flex-direction: column;
|
| 896 |
+
align-items: center;
|
| 897 |
+
gap: 3px;
|
| 898 |
+
padding: 0 24px;
|
| 899 |
+
}
|
| 900 |
+
|
| 901 |
+
.stat-num {
|
| 902 |
+
font-family: 'Space Grotesk', sans-serif;
|
| 903 |
+
font-size: 22px;
|
| 904 |
+
font-weight: 700;
|
| 905 |
+
color: var(--text-1);
|
| 906 |
+
background: linear-gradient(135deg, var(--accent-light), #c7d2fe);
|
| 907 |
+
-webkit-background-clip: text;
|
| 908 |
+
-webkit-text-fill-color: transparent;
|
| 909 |
+
background-clip: text;
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
.stat-label { font-size: 11px; color: var(--text-3); font-weight: 500; letter-spacing: 0.04em; }
|
| 913 |
+
.stat-divider { width: 1px; height: 32px; background: var(--border); }
|
| 914 |
+
|
| 915 |
+
/* ββ EXPORT BUTTONS UPGRADE ββββββββββββββββββββββββββββ */
|
| 916 |
+
.export-section {
|
| 917 |
+
margin-top: 20px;
|
| 918 |
+
padding-top: 20px;
|
| 919 |
+
border-top: 1px solid var(--border);
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
.export-header {
|
| 923 |
+
display: flex;
|
| 924 |
+
justify-content: space-between;
|
| 925 |
+
align-items: center;
|
| 926 |
+
margin-bottom: 12px;
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
.export-label {
|
| 930 |
+
font-size: 13px;
|
| 931 |
+
font-weight: 600;
|
| 932 |
+
color: var(--text-2);
|
| 933 |
+
letter-spacing: 0.02em;
|
| 934 |
+
}
|
| 935 |
+
|
| 936 |
+
.export-hint { font-size: 11px; color: var(--text-3); }
|
| 937 |
+
|
| 938 |
+
.export-btns {
|
| 939 |
+
display: flex;
|
| 940 |
+
gap: 10px;
|
| 941 |
+
flex-wrap: wrap;
|
| 942 |
+
}
|
| 943 |
+
|
| 944 |
+
.btn-export {
|
| 945 |
+
display: flex;
|
| 946 |
+
align-items: center;
|
| 947 |
+
gap: 12px;
|
| 948 |
+
padding: 12px 20px;
|
| 949 |
+
border-radius: 12px;
|
| 950 |
+
font-size: 13px;
|
| 951 |
+
cursor: pointer;
|
| 952 |
+
border: 1px solid var(--border);
|
| 953 |
+
background: var(--bg-card-2);
|
| 954 |
+
transition: all 0.2s;
|
| 955 |
+
font-family: 'Inter', sans-serif;
|
| 956 |
+
flex: 1;
|
| 957 |
+
min-width: 120px;
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
.export-icon {
|
| 961 |
+
width: 36px;
|
| 962 |
+
height: 36px;
|
| 963 |
+
border-radius: 8px;
|
| 964 |
+
display: flex;
|
| 965 |
+
align-items: center;
|
| 966 |
+
justify-content: center;
|
| 967 |
+
flex-shrink: 0;
|
| 968 |
+
transition: transform 0.2s;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
.btn-export:hover .export-icon { transform: scale(1.1); }
|
| 972 |
+
.btn-export:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
|
| 973 |
+
|
| 974 |
+
.export-info { display: flex; flex-direction: column; gap: 2px; }
|
| 975 |
+
.export-name { font-size: 13px; font-weight: 600; color: var(--text-1); }
|
| 976 |
+
.export-ext { font-size: 11px; color: var(--text-3); font-family: var(--font-mono, monospace); }
|
| 977 |
+
|
| 978 |
+
.btn-export-txt { border-color: rgba(99,102,241,0.25); }
|
| 979 |
+
.btn-export-txt .export-icon { background: rgba(99,102,241,0.12); color: #a5b4fc; }
|
| 980 |
+
.btn-export-txt:hover { border-color: #6366f1; background: rgba(99,102,241,0.06); }
|
| 981 |
+
.btn-export-txt .export-name { color: #a5b4fc; }
|
| 982 |
+
|
| 983 |
+
.btn-export-srt { border-color: rgba(16,185,129,0.25); }
|
| 984 |
+
.btn-export-srt .export-icon { background: rgba(16,185,129,0.12); color: #6ee7b7; }
|
| 985 |
+
.btn-export-srt:hover { border-color: #10b981; background: rgba(16,185,129,0.06); }
|
| 986 |
+
.btn-export-srt .export-name { color: #6ee7b7; }
|
| 987 |
+
|
| 988 |
+
.btn-export-pdf { border-color: rgba(239,68,68,0.25); }
|
| 989 |
+
.btn-export-pdf .export-icon { background: rgba(239,68,68,0.12); color: #fca5a5; }
|
| 990 |
+
.btn-export-pdf:hover { border-color: #ef4444; background: rgba(239,68,68,0.06); }
|
| 991 |
+
.btn-export-pdf .export-name { color: #fca5a5; }
|
| 992 |
+
|
| 993 |
+
/* ββ FOOTER UPGRADE ββββββββββββββββββββββββββββββββββββ */
|
| 994 |
+
.footer {
|
| 995 |
+
border-top: 1px solid var(--border);
|
| 996 |
+
padding: 0;
|
| 997 |
+
background: var(--bg-card);
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
.footer-inner {
|
| 1001 |
+
max-width: 960px;
|
| 1002 |
+
margin: 0 auto;
|
| 1003 |
+
padding: 48px 24px 40px;
|
| 1004 |
+
display: grid;
|
| 1005 |
+
grid-template-columns: 1.5fr 1fr 1fr;
|
| 1006 |
+
gap: 40px;
|
| 1007 |
+
}
|
| 1008 |
+
|
| 1009 |
+
@media (max-width: 640px) {
|
| 1010 |
+
.footer-inner { grid-template-columns: 1fr; gap: 28px; }
|
| 1011 |
+
.hero-stats { flex-wrap: wrap; padding: 14px 16px; }
|
| 1012 |
+
.stat { padding: 0 12px; }
|
| 1013 |
+
.header-link { display: none; }
|
| 1014 |
+
}
|
| 1015 |
+
|
| 1016 |
+
.footer-brand { display: flex; flex-direction: column; gap: 12px; }
|
| 1017 |
+
.footer-logo { display: flex; align-items: center; gap: 10px; }
|
| 1018 |
+
|
| 1019 |
+
.footer-tagline {
|
| 1020 |
+
font-size: 13px;
|
| 1021 |
+
color: var(--text-3);
|
| 1022 |
+
line-height: 1.7;
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
.footer-by {
|
| 1026 |
+
font-size: 13px;
|
| 1027 |
+
color: var(--text-3);
|
| 1028 |
+
}
|
| 1029 |
+
|
| 1030 |
+
.footer-a {
|
| 1031 |
+
color: var(--accent-light);
|
| 1032 |
+
text-decoration: none;
|
| 1033 |
+
}
|
| 1034 |
+
.footer-a:hover { text-decoration: underline; }
|
| 1035 |
+
|
| 1036 |
+
.footer-stack-label {
|
| 1037 |
+
font-size: 11px;
|
| 1038 |
+
font-weight: 600;
|
| 1039 |
+
text-transform: uppercase;
|
| 1040 |
+
letter-spacing: 0.08em;
|
| 1041 |
+
color: var(--text-3);
|
| 1042 |
+
margin-bottom: 12px;
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
.footer-stack {
|
| 1046 |
+
display: flex;
|
| 1047 |
+
flex-wrap: wrap;
|
| 1048 |
+
gap: 6px;
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
.footer-links { display: flex; flex-direction: column; gap: 10px; }
|
| 1052 |
+
|
| 1053 |
+
.footer-link-item {
|
| 1054 |
+
display: flex;
|
| 1055 |
+
align-items: center;
|
| 1056 |
+
gap: 8px;
|
| 1057 |
+
font-size: 13px;
|
| 1058 |
+
color: var(--text-2);
|
| 1059 |
+
text-decoration: none;
|
| 1060 |
+
transition: color 0.2s;
|
| 1061 |
+
}
|
| 1062 |
+
.footer-link-item:hover { color: var(--accent-light); }
|
| 1063 |
+
|
| 1064 |
+
.footer-bottom {
|
| 1065 |
+
border-top: 1px solid var(--border);
|
| 1066 |
+
padding: 16px 24px;
|
| 1067 |
+
max-width: 960px;
|
| 1068 |
+
margin: 0 auto;
|
| 1069 |
+
display: flex;
|
| 1070 |
+
justify-content: space-between;
|
| 1071 |
+
align-items: center;
|
| 1072 |
+
font-size: 11px;
|
| 1073 |
+
color: var(--text-3);
|
| 1074 |
+
flex-wrap: wrap;
|
| 1075 |
+
gap: 8px;
|
| 1076 |
+
}
|
requirements.txt
CHANGED
|
@@ -1,6 +1,3 @@
|
|
| 1 |
-
# requirements.txt
|
| 2 |
-
# Install with: pip install -r requirements.txt
|
| 3 |
-
|
| 4 |
flask==3.0.3
|
| 5 |
flask-cors==4.0.1
|
| 6 |
SpeechRecognition==3.11.0
|
|
@@ -8,3 +5,4 @@ pydub==0.25.1
|
|
| 8 |
openai-whisper
|
| 9 |
numpy
|
| 10 |
demucs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
flask==3.0.3
|
| 2 |
flask-cors==4.0.1
|
| 3 |
SpeechRecognition==3.11.0
|
|
|
|
| 5 |
openai-whisper
|
| 6 |
numpy
|
| 7 |
demucs
|
| 8 |
+
deep-translator==1.11.4
|