File size: 9,704 Bytes
5817aa9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | import os, re, json, traceback, subprocess
from pathlib import Path
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
BASE_DIR = Path(__file__).parent
STATIC_DIR = BASE_DIR / "static"
VIDEOS_DIR = STATIC_DIR / "videos"
UPLOAD_DIR = BASE_DIR / "uploads"
MODELS_DIR = BASE_DIR / "models"
for p in [STATIC_DIR, VIDEOS_DIR, UPLOAD_DIR, MODELS_DIR, STATIC_DIR / "models"]:
p.mkdir(exist_ok=True, parents=True)
app = Flask(__name__, static_folder=str(STATIC_DIR))
CORS(app)
whisper_model = None
@app.route("/")
def index():
return send_from_directory(str(STATIC_DIR), "index.html")
@app.route("/static/<path:path>")
def static_files(path):
return send_from_directory(str(STATIC_DIR), path)
@app.route("/api/videos")
def list_videos():
videos = []
for f in sorted(VIDEOS_DIR.glob("*.mp4")):
title = f.stem.replace("_", " ").title()
videos.append({"filename": f.name, "title": title, "url": f"/static/videos/{f.name}"})
return jsonify({"videos": videos})
@app.route("/api/process-video", methods=["POST"])
def process_video():
try:
video_path = None
local_video = request.form.get("local_video", "").strip()
if local_video:
candidate = (VIDEOS_DIR / local_video).resolve()
if str(candidate).startswith(str(VIDEOS_DIR.resolve())) and candidate.exists():
video_path = candidate
if video_path is None and "file" in request.files and request.files["file"].filename:
uploaded = request.files["file"]
safe_name = re.sub(r"[^A-Za-z0-9_.-]", "_", uploaded.filename)
video_path = UPLOAD_DIR / safe_name
uploaded.save(video_path)
if video_path is None:
url = request.form.get("url", "").strip()
if url:
video_path = download_video_from_url(url)
if not video_path:
return jsonify({"error": "No video found. Choose an internal video or upload an MP4."}), 400
stt = run_stt(str(video_path))
transcript = stt["text"]
segments = stt["segments"]
tokens = clean_tokens(transcript)
signs = text_to_asl_glosses(tokens)
timeline = build_sign_timeline(segments, signs)
summary = make_simple_summary(transcript, signs)
return jsonify({
"video_used": str(Path(video_path).name),
"transcript": transcript,
"segments": segments,
"tokens": tokens[:120],
"signs": signs,
"timeline": timeline,
"summary": summary,
"metrics": {
"stt": "Whisper base",
"glosses": len(signs),
"avatar": "Real GLB avatar + gloss animation"
}
})
except Exception as e:
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@app.route("/api/ask", methods=["POST"])
def ask():
data = request.get_json(force=True) or {}
q = data.get("question", "").lower().strip()
transcript = data.get("transcript", "").strip()
if not q:
return jsonify({"answer": "Please write a question."})
if not transcript:
return jsonify({"answer": "Process a video first."})
q_tokens = set(clean_tokens(q))
sentences = re.split(r"(?<=[.!?])\s+", transcript)
ranked = []
for s in sentences:
score = len(q_tokens.intersection(clean_tokens(s)))
if score:
ranked.append((score, s))
ranked.sort(reverse=True)
answer = ranked[0][1] if ranked else transcript[:350] + ("..." if len(transcript) > 350 else "")
return jsonify({"answer": answer})
def download_video_from_url(url: str):
out = UPLOAD_DIR / "downloaded_video.%(ext)s"
cmd = ["yt-dlp", "--no-playlist", "--socket-timeout", "20", "--retries", "2", "-f", "best[ext=mp4]/best", "--merge-output-format", "mp4", "-o", str(out), url]
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
if r.returncode != 0:
print(r.stderr[:800])
return None
files = list(UPLOAD_DIR.glob("downloaded_video.*"))
return files[0] if files else None
except Exception as e:
print("yt-dlp error:", e)
return None
def run_stt(video_path: str) -> dict:
global whisper_model
try:
import whisper
if whisper_model is None:
print("Loading Whisper base model. This can take time the first time...")
whisper_model = whisper.load_model("base")
print("Transcribing:", video_path)
result = whisper_model.transcribe(video_path, language="en", fp16=False)
text = result.get("text", "").strip()
segments = []
for seg in result.get("segments", []):
segments.append({
"start": float(seg.get("start", 0)),
"end": float(seg.get("end", 0)),
"text": seg.get("text", "").strip()
})
return {"text": text if text else "No speech detected in this video.", "segments": segments}
except Exception as e:
print("Whisper failed:", e)
return {"text": "Automatic transcription failed. Install dependencies with: pip install -r requirements.txt. Also make sure ffmpeg is installed.", "segments": []}
def clean_tokens(text: str):
text = text.lower()
text = re.sub(r"[^a-z0-9\s]", " ", text)
tokens = text.split()
stop = {"the","a","an","and","or","to","of","in","on","for","with","is","are","was","were","be","been","being","this","that","it","we","you","your","our","i","they","them","as","at","by","from","can","will","would","should","about","into","than","then","so","if","not","do","does","have","has","had","there","their","what","when","where","why","how","also","just","very","really","more","most","one","two","three","first","second","now","today","here","like"}
return [t for t in tokens if len(t) > 2 and t not in stop]
ASL_MAP = {
"linkedin":"LINKEDIN", "profile":"PROFILE", "picture":"PICTURE", "photo":"PHOTO", "professional":"PROFESSIONAL", "identity":"IDENTITY", "headline":"HEADLINE", "summary":"SUMMARY", "resume":"RESUME", "cv":"RESUME", "career":"CAREER", "job":"JOB", "work":"WORK", "experience":"EXPERIENCE", "education":"EDUCATION", "skill":"SKILL", "skills":"SKILL", "network":"NETWORK", "connect":"CONNECT", "connection":"CONNECT", "message":"MESSAGE", "company":"COMPANY", "business":"BUSINESS", "interview":"INTERVIEW", "recruiter":"RECRUITER", "hire":"HIRE", "hiring":"HIRE",
"math":"MATH", "mathematics":"MATH", "number":"NUMBER", "numbers":"NUMBER", "equation":"EQUATION", "equations":"EQUATION", "linear":"LINEAR", "system":"SYSTEM", "systems":"SYSTEM", "solve":"SOLVE", "solution":"SOLUTION", "variable":"VARIABLE", "matrix":"MATRIX", "addition":"ADDITION", "add":"ADDITION", "subtraction":"SUBTRACT", "subtract":"SUBTRACT", "multiply":"MULTIPLY", "division":"DIVIDE", "equal":"EQUAL", "graph":"GRAPH", "function":"FUNCTION",
"science":"SCIENCE", "cell":"CELL", "energy":"ENERGY", "plant":"PLANT", "photosynthesis":"PHOTOSYNTHESIS", "learn":"LEARN", "learning":"LEARN", "lesson":"LESSON", "student":"STUDENT", "teacher":"TEACHER", "school":"SCHOOL", "course":"COURSE", "hello":"HELLO", "welcome":"WELCOME", "important":"IMPORTANT", "good":"GOOD", "great":"GREAT", "clear":"CLEAR", "example":"EXAMPLE", "result":"RESULT", "calculate":"CALCULATE", "understand":"UNDERSTAND", "explain":"EXPLAIN", "help":"HELP"
}
def text_to_asl_glosses(tokens):
signs, seen = [], set()
for t in tokens:
gloss = ASL_MAP.get(t)
if gloss and gloss not in seen:
signs.append({"gloss": gloss, "source_word": t, "confidence": 0.88, "mode": "dictionary"})
seen.add(gloss)
if len(signs) < 5:
for t in tokens[:12]:
gloss = t.upper()
if gloss not in seen:
signs.append({"gloss": gloss, "source_word": t, "confidence": 0.60, "mode": "finger-spelling"})
seen.add(gloss)
if len(signs) >= 12:
break
return signs[:18]
def build_sign_timeline(segments, global_signs):
timeline = []
for seg in segments:
tokens = clean_tokens(seg.get("text", ""))
local_signs = text_to_asl_glosses(tokens)
if not local_signs:
continue
start, end = float(seg["start"]), float(seg["end"])
duration = max(end - start, 0.8)
step = duration / len(local_signs)
for i, sign in enumerate(local_signs):
timeline.append({
"start": start + i * step,
"end": start + (i + 1) * step,
"gloss": sign["gloss"],
"source_word": sign["source_word"],
"mode": sign["mode"]
})
if timeline:
return timeline[:120]
# fallback cycle when Whisper segments fail
t = 0.0
for sign in global_signs:
timeline.append({"start": t, "end": t + 1.4, "gloss": sign["gloss"], "source_word": sign["source_word"], "mode": sign["mode"]})
t += 1.4
return timeline
def make_simple_summary(transcript, signs):
return {
"first_words": transcript[:220] + ("..." if len(transcript) > 220 else ""),
"main_glosses": [s["gloss"] for s in signs[:10]],
"note": "The 3D avatar uses a real GLB model. It follows glosses generated from Whisper transcript. This is an MVP rendering layer, not a full ASL production engine yet."
}
if __name__ == "__main__":
print("\n🚀 iLearning SignAI GLB avatar demo")
print("Open: http://0.0.0.0:7860\n")
app.run(host="0.0.0.0", port=7860, debug=False)
|