Spaces:
Running
Running
Commit Β·
bb5dd0a
1
Parent(s): 18dd411
Deploy SignApp main app
Browse files- .gitattributes +1 -0
- .gitignore +3 -0
- Dockerfile +14 -0
- README.md +19 -4
- requirements.txt +8 -0
- src/sign_app/__init__.py +10 -0
- src/sign_app/api.py +243 -0
- src/sign_app/audio/__init__.py +1 -0
- src/sign_app/disfluency/__init__.py +1 -0
- src/sign_app/disfluency/inference.py +37 -0
- src/sign_app/disfluency/training.py +121 -0
- src/sign_app/seed_signs.py +316 -0
- src/sign_app/sign_language_text/__init__.py +1 -0
- src/sign_app/sign_language_text/gloss_converter.py +501 -0
- src/sign_app/sign_language_text/inference.py +58 -0
- src/sign_app/sign_language_text/training.py +127 -0
- src/sign_app/ui/__init__.py +1 -0
- src/sign_app/ui/avatar.glb +3 -0
- src/sign_app/ui/fingerspellDictionary.js +323 -0
- src/sign_app/ui/index.html +511 -0
- src/sign_app/ui/main.js +742 -0
- src/sign_app/ui/signEngine.js +438 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.glb filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
*.egg-info/
|
Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY src/ src/
|
| 9 |
+
RUN mkdir -p uploads
|
| 10 |
+
|
| 11 |
+
EXPOSE 7860
|
| 12 |
+
|
| 13 |
+
CMD ["uvicorn", "src.sign_app.api:app", "--host", "0.0.0.0", "--port", "7860"]
|
| 14 |
+
|
README.md
CHANGED
|
@@ -1,10 +1,25 @@
|
|
| 1 |
---
|
| 2 |
title: SignApp
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: SignApp
|
| 3 |
+
emoji: π€
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# SignApp
|
| 12 |
+
|
| 13 |
+
Main SignApp UI/API deployment. This Space should stay lightweight and call the
|
| 14 |
+
separate model Spaces for Whisper and disfluency removal.
|
| 15 |
+
|
| 16 |
+
Required Space secrets:
|
| 17 |
+
|
| 18 |
+
- `MONGODB_URI`
|
| 19 |
+
- `WHISPER_API_URL`
|
| 20 |
+
- `DISFLUENCY_API_URL`
|
| 21 |
+
|
| 22 |
+
Optional Space secrets:
|
| 23 |
+
|
| 24 |
+
- `HF_TOKEN` for private/protected model Spaces or better public Space rate limits
|
| 25 |
+
- `REMOTE_API_TIMEOUT`, default `300`
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.104.0
|
| 2 |
+
uvicorn>=0.24.0
|
| 3 |
+
python-multipart>=0.0.6
|
| 4 |
+
python-dotenv>=1.0.0
|
| 5 |
+
nltk>=3.8.1
|
| 6 |
+
requests>=2.31.0
|
| 7 |
+
pymongo>=4.6.0
|
| 8 |
+
|
src/sign_app/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SignApp - Audio processing pipeline for sign language recognition.
|
| 3 |
+
|
| 4 |
+
This module provides functionality for:
|
| 5 |
+
- Audio transcription using Whisper
|
| 6 |
+
- Disfluency removal from transcriptions
|
| 7 |
+
- Further processing for sign language recognition
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
__version__ = "0.1.0"
|
src/sign_app/api.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import shutil
|
| 3 |
+
from contextlib import asynccontextmanager
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import requests
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
from fastapi import FastAPI, File, HTTPException, UploadFile
|
| 9 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
+
from fastapi.responses import FileResponse
|
| 11 |
+
from fastapi.staticfiles import StaticFiles
|
| 12 |
+
from pydantic import BaseModel
|
| 13 |
+
from pymongo import MongoClient
|
| 14 |
+
|
| 15 |
+
load_dotenv()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
MONGODB_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017/")
|
| 19 |
+
WHISPER_MODEL_SIZE = os.getenv("WHISPER_MODEL", "small")
|
| 20 |
+
|
| 21 |
+
WHISPER_API_URL = os.getenv("WHISPER_API_URL", "").strip().rstrip("/")
|
| 22 |
+
DISFLUENCY_API_URL = os.getenv("DISFLUENCY_API_URL", "").strip().rstrip("/")
|
| 23 |
+
REMOTE_API_TIMEOUT = int(os.getenv("REMOTE_API_TIMEOUT", "300"))
|
| 24 |
+
HF_TOKEN = os.getenv("HF_TOKEN", "").strip()
|
| 25 |
+
|
| 26 |
+
UPLOAD_DIR = Path("uploads")
|
| 27 |
+
UPLOAD_DIR.mkdir(exist_ok=True)
|
| 28 |
+
|
| 29 |
+
UI_DIR = Path(__file__).parent / "ui"
|
| 30 |
+
|
| 31 |
+
client = MongoClient(MONGODB_URI)
|
| 32 |
+
db = client["SignApp"]
|
| 33 |
+
sign_rules_col = db["sign_rules"]
|
| 34 |
+
fingerspell_col = db["fingerspelling"]
|
| 35 |
+
|
| 36 |
+
_whisper_model = None
|
| 37 |
+
_disfluency_fn = None
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _auth_headers() -> dict[str, str]:
|
| 41 |
+
if not HF_TOKEN:
|
| 42 |
+
return {}
|
| 43 |
+
return {"Authorization": f"Bearer {HF_TOKEN}"}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def get_whisper():
|
| 47 |
+
global _whisper_model
|
| 48 |
+
if _whisper_model is None:
|
| 49 |
+
import whisper
|
| 50 |
+
|
| 51 |
+
_whisper_model = whisper.load_model(WHISPER_MODEL_SIZE)
|
| 52 |
+
return _whisper_model
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def get_disfluency_fn():
|
| 56 |
+
global _disfluency_fn
|
| 57 |
+
if _disfluency_fn is None:
|
| 58 |
+
from .disfluency.inference import remove_disfluency
|
| 59 |
+
|
| 60 |
+
_disfluency_fn = remove_disfluency
|
| 61 |
+
return _disfluency_fn
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def transcribe_audio(file_path: Path) -> dict:
|
| 65 |
+
if WHISPER_API_URL:
|
| 66 |
+
with file_path.open("rb") as audio_file:
|
| 67 |
+
response = requests.post(
|
| 68 |
+
f"{WHISPER_API_URL}/transcribe/",
|
| 69 |
+
headers=_auth_headers(),
|
| 70 |
+
files={"file": (file_path.name, audio_file, "audio/webm")},
|
| 71 |
+
timeout=REMOTE_API_TIMEOUT,
|
| 72 |
+
)
|
| 73 |
+
response.raise_for_status()
|
| 74 |
+
data = response.json()
|
| 75 |
+
return {
|
| 76 |
+
"text": data.get("text", ""),
|
| 77 |
+
"language": data.get("language", "en"),
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
whisper_model = get_whisper()
|
| 81 |
+
result = whisper_model.transcribe(str(file_path), language="en")
|
| 82 |
+
return {
|
| 83 |
+
"text": result["text"],
|
| 84 |
+
"language": result["language"],
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def clean_disfluency(text: str) -> str:
|
| 89 |
+
if DISFLUENCY_API_URL:
|
| 90 |
+
response = requests.post(
|
| 91 |
+
f"{DISFLUENCY_API_URL}/clean/",
|
| 92 |
+
headers=_auth_headers(),
|
| 93 |
+
json={"text": text},
|
| 94 |
+
timeout=REMOTE_API_TIMEOUT,
|
| 95 |
+
)
|
| 96 |
+
response.raise_for_status()
|
| 97 |
+
data = response.json()
|
| 98 |
+
return data.get("cleaned_text", "").strip()
|
| 99 |
+
|
| 100 |
+
return get_disfluency_fn()(text)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@asynccontextmanager
|
| 104 |
+
async def lifespan(app: FastAPI):
|
| 105 |
+
if not WHISPER_API_URL:
|
| 106 |
+
print("Loading local Whisper model on startup...")
|
| 107 |
+
get_whisper()
|
| 108 |
+
else:
|
| 109 |
+
print(f"Using remote Whisper API: {WHISPER_API_URL}")
|
| 110 |
+
|
| 111 |
+
if not DISFLUENCY_API_URL:
|
| 112 |
+
print("Loading local disfluency model on startup...")
|
| 113 |
+
get_disfluency_fn()
|
| 114 |
+
else:
|
| 115 |
+
print(f"Using remote disfluency API: {DISFLUENCY_API_URL}")
|
| 116 |
+
|
| 117 |
+
print("SignApp startup complete.")
|
| 118 |
+
yield
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
app = FastAPI(title="SignApp", version="0.1.0", lifespan=lifespan)
|
| 122 |
+
|
| 123 |
+
app.add_middleware(
|
| 124 |
+
CORSMiddleware,
|
| 125 |
+
allow_origins=["*"],
|
| 126 |
+
allow_credentials=True,
|
| 127 |
+
allow_methods=["*"],
|
| 128 |
+
allow_headers=["*"],
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
from .sign_language_text.gloss_converter import convert_to_sign_gloss
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class TextInput(BaseModel):
|
| 135 |
+
text: str
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def build_sign_sequence(gloss_tokens: list[str]) -> list[dict]:
|
| 139 |
+
"""Look up each gloss token in MongoDB sign_rules, fall back to fingerspelling."""
|
| 140 |
+
sign_sequence = []
|
| 141 |
+
|
| 142 |
+
for word in gloss_tokens:
|
| 143 |
+
rule = sign_rules_col.find_one({"sign": word})
|
| 144 |
+
|
| 145 |
+
if rule:
|
| 146 |
+
sign_sequence.append(
|
| 147 |
+
{
|
| 148 |
+
"type": "sign",
|
| 149 |
+
"gloss": word,
|
| 150 |
+
"handshape": rule["handshape"],
|
| 151 |
+
"location": rule["location"],
|
| 152 |
+
"movement": rule["movement"],
|
| 153 |
+
"expression": rule.get("expression", "neutral"),
|
| 154 |
+
}
|
| 155 |
+
)
|
| 156 |
+
else:
|
| 157 |
+
for letter in word:
|
| 158 |
+
finger = fingerspell_col.find_one({"letter": letter.upper()})
|
| 159 |
+
if finger:
|
| 160 |
+
sign_sequence.append(
|
| 161 |
+
{
|
| 162 |
+
"type": "fingerspell",
|
| 163 |
+
"letter": letter.upper(),
|
| 164 |
+
"handshape": finger["handshape"],
|
| 165 |
+
"location": "neutral_space",
|
| 166 |
+
"movement": finger.get("movement") or "none",
|
| 167 |
+
}
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
return sign_sequence
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def text_pipeline(text: str) -> dict:
|
| 174 |
+
cleaned_text = clean_disfluency(text)
|
| 175 |
+
sign_friendly_text = convert_to_sign_gloss(cleaned_text)
|
| 176 |
+
sign_sequence = build_sign_sequence(sign_friendly_text)
|
| 177 |
+
|
| 178 |
+
return {
|
| 179 |
+
"cleaned_transcription": cleaned_text,
|
| 180 |
+
"sign_friendly_text": sign_friendly_text,
|
| 181 |
+
"sign_sequence": sign_sequence,
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
@app.get("/health")
|
| 186 |
+
def health():
|
| 187 |
+
return {
|
| 188 |
+
"status": "ok",
|
| 189 |
+
"whisper": "remote" if WHISPER_API_URL else "local",
|
| 190 |
+
"disfluency": "remote" if DISFLUENCY_API_URL else "local",
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
@app.post("/voice-to-text/")
|
| 195 |
+
def voice_to_text_endpoint(file: UploadFile = File(...)):
|
| 196 |
+
"""Full pipeline: audio -> transcription -> gloss -> sign sequence."""
|
| 197 |
+
file_path = UPLOAD_DIR / (file.filename or "recording.webm")
|
| 198 |
+
|
| 199 |
+
try:
|
| 200 |
+
with file_path.open("wb") as audio_file:
|
| 201 |
+
shutil.copyfileobj(file.file, audio_file)
|
| 202 |
+
|
| 203 |
+
transcription_result = transcribe_audio(file_path)
|
| 204 |
+
transcription = transcription_result["text"]
|
| 205 |
+
language = transcription_result["language"]
|
| 206 |
+
|
| 207 |
+
result = text_pipeline(transcription)
|
| 208 |
+
return {
|
| 209 |
+
"language": language,
|
| 210 |
+
"raw_transcription": transcription,
|
| 211 |
+
**result,
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
except requests.RequestException as exc:
|
| 215 |
+
raise HTTPException(status_code=502, detail=f"Remote model service failed: {exc}") from exc
|
| 216 |
+
except Exception as exc:
|
| 217 |
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
| 218 |
+
finally:
|
| 219 |
+
if file_path.exists():
|
| 220 |
+
file_path.unlink()
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
@app.post("/text-to-sign/")
|
| 224 |
+
def text_to_sign_endpoint(body: TextInput):
|
| 225 |
+
"""Text-only pipeline: text -> gloss -> sign sequence."""
|
| 226 |
+
text = body.text.strip()
|
| 227 |
+
if not text:
|
| 228 |
+
raise HTTPException(status_code=400, detail="Text is empty")
|
| 229 |
+
|
| 230 |
+
try:
|
| 231 |
+
return text_pipeline(text)
|
| 232 |
+
except requests.RequestException as exc:
|
| 233 |
+
raise HTTPException(status_code=502, detail=f"Remote model service failed: {exc}") from exc
|
| 234 |
+
except Exception as exc:
|
| 235 |
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
@app.get("/")
|
| 239 |
+
def serve_ui():
|
| 240 |
+
return FileResponse(UI_DIR / "index.html")
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
app.mount("/", StaticFiles(directory=str(UI_DIR)), name="ui")
|
src/sign_app/audio/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Audio processing module using Whisper."""
|
src/sign_app/disfluency/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Disfluency removal module for cleaning speech transcriptions."""
|
src/sign_app/disfluency/inference.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from transformers import T5Tokenizer, T5ForConditionalGeneration
|
| 3 |
+
|
| 4 |
+
MODEL_PATH = "./speechCleaner_t5_model"
|
| 5 |
+
|
| 6 |
+
# Load tokenizer & model
|
| 7 |
+
tokenizer = T5Tokenizer.from_pretrained(MODEL_PATH)
|
| 8 |
+
model = T5ForConditionalGeneration.from_pretrained(MODEL_PATH)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 12 |
+
model.to(device)
|
| 13 |
+
model.eval()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def remove_disfluency(text: str) -> str:
|
| 17 |
+
inputs = tokenizer(
|
| 18 |
+
"clean speech: " + text,
|
| 19 |
+
return_tensors="pt",
|
| 20 |
+
truncation=True,
|
| 21 |
+
padding=True
|
| 22 |
+
).to(device)
|
| 23 |
+
|
| 24 |
+
with torch.no_grad():
|
| 25 |
+
outputs = model.generate(
|
| 26 |
+
**inputs,
|
| 27 |
+
max_length=256,
|
| 28 |
+
num_beams=4,
|
| 29 |
+
early_stopping=True
|
| 30 |
+
)
|
| 31 |
+
cleaned_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 32 |
+
return cleaned_text.strip()
|
| 33 |
+
|
| 34 |
+
# Test the disfluency removal on some example sentences
|
| 35 |
+
if __name__ == "__main__":
|
| 36 |
+
text = "I uh want to go to the store"
|
| 37 |
+
print(remove_disfluency(text))
|
src/sign_app/disfluency/training.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datasets import load_dataset
|
| 2 |
+
from transformers import T5Tokenizer, T5ForConditionalGeneration, Seq2SeqTrainer, Seq2SeqTrainingArguments, DataCollatorForSeq2Seq
|
| 3 |
+
import mlflow
|
| 4 |
+
import evaluate
|
| 5 |
+
import nltk
|
| 6 |
+
nltk.download('punkt')
|
| 7 |
+
bleu = evaluate.load("bleu")
|
| 8 |
+
rouge = evaluate.load("rouge")
|
| 9 |
+
|
| 10 |
+
base_model = "t5-base" # You can choose a larger model like "t5-base" or "t5-large" if you have the resources
|
| 11 |
+
|
| 12 |
+
tokenizer = T5Tokenizer.from_pretrained(base_model)
|
| 13 |
+
transformer_model = T5ForConditionalGeneration.from_pretrained(base_model)
|
| 14 |
+
|
| 15 |
+
switchboard_dataset = load_dataset("amaai-lab/DisfluencySpeech")
|
| 16 |
+
|
| 17 |
+
def keep_only_text_columns(example):
|
| 18 |
+
return {
|
| 19 |
+
"input_text": example["transcript_a"],
|
| 20 |
+
"target_text": example["transcript_c"]
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
dataset = switchboard_dataset.map(
|
| 24 |
+
keep_only_text_columns,
|
| 25 |
+
remove_columns=switchboard_dataset["train"].column_names
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
def is_valid(example):
|
| 29 |
+
return (
|
| 30 |
+
example["input_text"] is not None
|
| 31 |
+
and example["target_text"] is not None
|
| 32 |
+
and example["input_text"].strip() != ""
|
| 33 |
+
and example["target_text"].strip() != ""
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
dataset = dataset.filter(is_valid)
|
| 37 |
+
|
| 38 |
+
encoding_max_length = 256
|
| 39 |
+
decoding_max_length = 256
|
| 40 |
+
|
| 41 |
+
def tokenize(sentences):
|
| 42 |
+
inputs = ["clean speech: " + text for text in sentences["input_text"]]
|
| 43 |
+
|
| 44 |
+
model_inputs = tokenizer(inputs, max_length=encoding_max_length, truncation=True, padding="max_length")
|
| 45 |
+
labels = tokenizer(
|
| 46 |
+
sentences["target_text"],
|
| 47 |
+
max_length=decoding_max_length,
|
| 48 |
+
truncation=True,
|
| 49 |
+
padding="max_length"
|
| 50 |
+
)
|
| 51 |
+
model_inputs["labels"] = labels["input_ids"]
|
| 52 |
+
return model_inputs
|
| 53 |
+
|
| 54 |
+
tokenized_dataset = dataset.map(
|
| 55 |
+
tokenize,batched=True,remove_columns=dataset["train"].column_names
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
data_collator = DataCollatorForSeq2Seq(tokenizer, model=transformer_model)
|
| 59 |
+
|
| 60 |
+
def compute_metrics(eval_pred):
|
| 61 |
+
predictions, labels = eval_pred
|
| 62 |
+
decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True)
|
| 63 |
+
labels = [[label if label != -100 else tokenizer.pad_token_id for label in l] for l in labels]
|
| 64 |
+
decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)
|
| 65 |
+
|
| 66 |
+
decoded_preds = [pred.strip() for pred in decoded_preds]
|
| 67 |
+
decoded_labels = [label.strip() for label in decoded_labels]
|
| 68 |
+
|
| 69 |
+
bleu_result = bleu.compute(predictions=decoded_preds, references=decoded_labels)
|
| 70 |
+
rouge_result = rouge.compute(predictions=decoded_preds, references=decoded_labels)
|
| 71 |
+
|
| 72 |
+
return {
|
| 73 |
+
"bleu": bleu_result["bleu"],
|
| 74 |
+
"rouge1": rouge_result["rouge1"],
|
| 75 |
+
"rouge2": rouge_result["rouge2"],
|
| 76 |
+
"rougeL": rouge_result["rougeL"]
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
training_args = Seq2SeqTrainingArguments(
|
| 80 |
+
output_dir="./speechCleaner_t5_model",
|
| 81 |
+
eval_strategy="epoch",
|
| 82 |
+
save_strategy="epoch",
|
| 83 |
+
learning_rate=3e-5,
|
| 84 |
+
per_device_train_batch_size=8,
|
| 85 |
+
per_device_eval_batch_size=8,
|
| 86 |
+
num_train_epochs=5,
|
| 87 |
+
weight_decay=0.01,
|
| 88 |
+
logging_steps=100,
|
| 89 |
+
save_total_limit=2,
|
| 90 |
+
fp16=True, # Set to True if you have a compatible GPU
|
| 91 |
+
report_to="mlflow",
|
| 92 |
+
predict_with_generate=True
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
trainer = Seq2SeqTrainer(
|
| 96 |
+
model=transformer_model,
|
| 97 |
+
args=training_args,
|
| 98 |
+
train_dataset=tokenized_dataset["train"],
|
| 99 |
+
eval_dataset=tokenized_dataset["validation"],
|
| 100 |
+
data_collator=data_collator,
|
| 101 |
+
compute_metrics=compute_metrics
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
mlflow.set_tracking_uri("file:./mlruns")
|
| 105 |
+
mlflow.set_experiment("speechCleaner_t5_model")
|
| 106 |
+
with mlflow.start_run():
|
| 107 |
+
trainer.train()
|
| 108 |
+
trainer.save_model("./SpeechCleaner_t5_model")
|
| 109 |
+
tokenizer.save_pretrained("./SpeechCleaner_t5_model")
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# Test the trained model on some example sentences
|
| 113 |
+
def clean_text(text: str) -> str:
|
| 114 |
+
inputs = tokenizer("clean speech: " + text, return_tensors="pt", truncation=True).input_ids.to(transformer_model.device)
|
| 115 |
+
outputs = transformer_model.generate(inputs, max_length=decoding_max_length, num_beams=4, early_stopping=True)
|
| 116 |
+
cleaned_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 117 |
+
return cleaned_text
|
| 118 |
+
|
| 119 |
+
print(clean_text("Yeah uh I I don't work but I used to work when I had two children"))
|
| 120 |
+
print(clean_text("I want to go to the store um to buy some groceries"))
|
| 121 |
+
print(clean_text("So uh the meeting is scheduled for uh next Monday at 10 am"))
|
src/sign_app/seed_signs.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Seed MongoDB with ASL sign rules, fingerspelling, handshapes, locations, and movements.
|
| 3 |
+
|
| 4 |
+
Usage:
|
| 5 |
+
uv run python -m src.sign_app.seed_signs
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
from pymongo import MongoClient
|
| 11 |
+
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
MONGODB_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017/")
|
| 15 |
+
|
| 16 |
+
client = MongoClient(MONGODB_URI)
|
| 17 |
+
db = client["SignApp"]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 21 |
+
# SIGN RULES β 60+ common ASL signs
|
| 22 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
+
|
| 24 |
+
SIGN_RULES = [
|
| 25 |
+
# Greetings & Social
|
| 26 |
+
{"sign": "HELLO", "handshape": "B", "location": "forehead", "movement": "wave"},
|
| 27 |
+
{"sign": "BYE", "handshape": "OPEN", "location": "neutral_space", "movement": "wave"},
|
| 28 |
+
{"sign": "THANK-YOU", "handshape": "FLAT", "location": "chin", "movement": "forward"},
|
| 29 |
+
{"sign": "PLEASE", "handshape": "FLAT", "location": "chest", "movement": "circle_clockwise"},
|
| 30 |
+
{"sign": "SORRY", "handshape": "S", "location": "chest", "movement": "circle_clockwise"},
|
| 31 |
+
{"sign": "YES", "handshape": "S", "location": "neutral_space", "movement": "nod"},
|
| 32 |
+
{"sign": "NO", "handshape": "POINT","location": "neutral_space", "movement": "side_to_side"},
|
| 33 |
+
{"sign": "OK", "handshape": "O", "location": "neutral_space", "movement": "none"},
|
| 34 |
+
{"sign": "EXCUSE", "handshape": "FLAT", "location": "chest", "movement": "forward"},
|
| 35 |
+
|
| 36 |
+
# Pronouns
|
| 37 |
+
{"sign": "I", "handshape": "POINT","location": "chest", "movement": "touch"},
|
| 38 |
+
{"sign": "YOU", "handshape": "POINT","location": "neutral_space", "movement": "forward"},
|
| 39 |
+
{"sign": "HE", "handshape": "POINT","location": "side", "movement": "forward"},
|
| 40 |
+
{"sign": "SHE", "handshape": "POINT","location": "side", "movement": "forward"},
|
| 41 |
+
{"sign": "WE", "handshape": "POINT","location": "shoulder", "movement": "circle_clockwise"},
|
| 42 |
+
{"sign": "THEY", "handshape": "POINT","location": "neutral_space", "movement": "side_to_side"},
|
| 43 |
+
{"sign": "MY", "handshape": "FLAT", "location": "chest", "movement": "tap"},
|
| 44 |
+
{"sign": "YOUR", "handshape": "FLAT", "location": "neutral_space", "movement": "forward"},
|
| 45 |
+
|
| 46 |
+
# Common Verbs
|
| 47 |
+
{"sign": "KNOW", "handshape": "B", "location": "temple", "movement": "tap"},
|
| 48 |
+
{"sign": "THINK", "handshape": "POINT","location": "forehead", "movement": "tap"},
|
| 49 |
+
{"sign": "WANT", "handshape": "CLAW", "location": "neutral_space", "movement": "forward"},
|
| 50 |
+
{"sign": "NEED", "handshape": "POINT","location": "neutral_space", "movement": "nod"},
|
| 51 |
+
{"sign": "LIKE", "handshape": "OPEN", "location": "chest", "movement": "forward"},
|
| 52 |
+
{"sign": "LOVE", "handshape": "FIST", "location": "chest", "movement": "tap"},
|
| 53 |
+
{"sign": "HELP", "handshape": "A", "location": "neutral_space", "movement": "up"},
|
| 54 |
+
{"sign": "SEE", "handshape": "V", "location": "nose", "movement": "forward"},
|
| 55 |
+
{"sign": "LOOK", "handshape": "V", "location": "nose", "movement": "forward"},
|
| 56 |
+
{"sign": "HEAR", "handshape": "POINT","location": "ear", "movement": "tap"},
|
| 57 |
+
{"sign": "LISTEN", "handshape": "C", "location": "ear", "movement": "tap"},
|
| 58 |
+
{"sign": "SAY", "handshape": "POINT","location": "chin", "movement": "forward"},
|
| 59 |
+
{"sign": "TELL", "handshape": "POINT","location": "chin", "movement": "forward"},
|
| 60 |
+
{"sign": "ASK", "handshape": "POINT","location": "neutral_space", "movement": "forward"},
|
| 61 |
+
{"sign": "GO", "handshape": "POINT","location": "neutral_space", "movement": "forward"},
|
| 62 |
+
{"sign": "COME", "handshape": "POINT","location": "neutral_space", "movement": "pull_in"},
|
| 63 |
+
{"sign": "EAT", "handshape": "FLAT", "location": "mouth", "movement": "tap"},
|
| 64 |
+
{"sign": "DRINK", "handshape": "C", "location": "mouth", "movement": "tap"},
|
| 65 |
+
{"sign": "WORK", "handshape": "S", "location": "neutral_space", "movement": "tap"},
|
| 66 |
+
{"sign": "LEARN", "handshape": "FLAT", "location": "forehead", "movement": "tap"},
|
| 67 |
+
{"sign": "TEACH", "handshape": "FLAT", "location": "forehead", "movement": "forward"},
|
| 68 |
+
{"sign": "UNDERSTAND", "handshape": "S", "location": "temple", "movement": "tap"},
|
| 69 |
+
{"sign": "FEEL", "handshape": "OPEN", "location": "chest", "movement": "up"},
|
| 70 |
+
{"sign": "GIVE", "handshape": "FLAT", "location": "neutral_space", "movement": "forward"},
|
| 71 |
+
{"sign": "TAKE", "handshape": "CLAW", "location": "neutral_space", "movement": "forward"},
|
| 72 |
+
{"sign": "MAKE", "handshape": "S", "location": "neutral_space", "movement": "twist"},
|
| 73 |
+
{"sign": "GET", "handshape": "CLAW", "location": "neutral_space", "movement": "forward"},
|
| 74 |
+
{"sign": "HAVE", "handshape": "B", "location": "chest", "movement": "tap"},
|
| 75 |
+
{"sign": "WAIT", "handshape": "OPEN", "location": "neutral_space", "movement": "none"},
|
| 76 |
+
{"sign": "STOP", "handshape": "FLAT", "location": "neutral_space", "movement": "down"},
|
| 77 |
+
{"sign": "START", "handshape": "POINT","location": "neutral_space", "movement": "twist"},
|
| 78 |
+
{"sign": "FINISH", "handshape": "OPEN", "location": "neutral_space", "movement": "down"},
|
| 79 |
+
{"sign": "TRY", "handshape": "S", "location": "neutral_space", "movement": "forward"},
|
| 80 |
+
{"sign": "WALK", "handshape": "FLAT", "location": "neutral_space", "movement": "forward"},
|
| 81 |
+
{"sign": "SIT", "handshape": "H", "location": "neutral_space", "movement": "down"},
|
| 82 |
+
{"sign": "STAND", "handshape": "V", "location": "neutral_space", "movement": "none"},
|
| 83 |
+
{"sign": "SIGN", "handshape": "POINT","location": "neutral_space", "movement": "circle_clockwise"},
|
| 84 |
+
{"sign": "WATCH", "handshape": "V", "location": "nose", "movement": "forward"},
|
| 85 |
+
{"sign": "OPEN", "handshape": "B", "location": "neutral_space", "movement": "side_to_side"},
|
| 86 |
+
{"sign": "CLOSE", "handshape": "B", "location": "neutral_space", "movement": "forward"},
|
| 87 |
+
{"sign": "BUY", "handshape": "FLAT", "location": "neutral_space", "movement": "forward"},
|
| 88 |
+
{"sign": "READ", "handshape": "V", "location": "neutral_space", "movement": "down"},
|
| 89 |
+
{"sign": "WRITE", "handshape": "POINT","location": "neutral_space", "movement": "down"},
|
| 90 |
+
|
| 91 |
+
# Question Words
|
| 92 |
+
{"sign": "WHAT", "handshape": "OPEN", "location": "neutral_space", "movement": "side_to_side"},
|
| 93 |
+
{"sign": "WHERE", "handshape": "POINT","location": "neutral_space", "movement": "side_to_side"},
|
| 94 |
+
{"sign": "WHEN", "handshape": "POINT","location": "neutral_space", "movement": "circle_clockwise"},
|
| 95 |
+
{"sign": "WHY", "handshape": "Y", "location": "forehead", "movement": "forward"},
|
| 96 |
+
{"sign": "HOW", "handshape": "FIST", "location": "neutral_space", "movement": "twist"},
|
| 97 |
+
{"sign": "WHO", "handshape": "L", "location": "chin", "movement": "tap"},
|
| 98 |
+
|
| 99 |
+
# Nouns
|
| 100 |
+
{"sign": "PERSON", "handshape": "FLAT", "location": "neutral_space", "movement": "down"},
|
| 101 |
+
{"sign": "PEOPLE", "handshape": "P", "location": "neutral_space", "movement": "circle_clockwise"},
|
| 102 |
+
{"sign": "FRIEND", "handshape": "X", "location": "neutral_space", "movement": "twist"},
|
| 103 |
+
{"sign": "FAMILY", "handshape": "F", "location": "neutral_space", "movement": "circle_clockwise"},
|
| 104 |
+
{"sign": "MOTHER", "handshape": "OPEN", "location": "chin", "movement": "tap"},
|
| 105 |
+
{"sign": "FATHER", "handshape": "OPEN", "location": "forehead", "movement": "tap"},
|
| 106 |
+
{"sign": "NAME", "handshape": "H", "location": "neutral_space", "movement": "tap"},
|
| 107 |
+
{"sign": "HOME", "handshape": "FLAT", "location": "chin", "movement": "tap"},
|
| 108 |
+
{"sign": "SCHOOL", "handshape": "FLAT", "location": "neutral_space", "movement": "double_tap"},
|
| 109 |
+
{"sign": "FOOD", "handshape": "FLAT", "location": "mouth", "movement": "tap"},
|
| 110 |
+
{"sign": "WATER", "handshape": "W", "location": "chin", "movement": "tap"},
|
| 111 |
+
{"sign": "MONEY", "handshape": "FLAT", "location": "neutral_space", "movement": "tap"},
|
| 112 |
+
{"sign": "TIME", "handshape": "POINT","location": "neutral_space", "movement": "tap"},
|
| 113 |
+
{"sign": "DAY", "handshape": "D", "location": "neutral_space", "movement": "down"},
|
| 114 |
+
{"sign": "TODAY", "handshape": "FLAT", "location": "neutral_space", "movement": "down"},
|
| 115 |
+
{"sign": "TOMORROW", "handshape": "A", "location": "chin", "movement": "forward"},
|
| 116 |
+
{"sign": "YESTERDAY", "handshape": "A", "location": "chin", "movement": "tap"},
|
| 117 |
+
{"sign": "MORNING", "handshape": "FLAT", "location": "neutral_space", "movement": "up"},
|
| 118 |
+
{"sign": "NIGHT", "handshape": "FLAT", "location": "neutral_space", "movement": "down"},
|
| 119 |
+
{"sign": "STORE", "handshape": "FLAT", "location": "neutral_space", "movement": "twist"},
|
| 120 |
+
|
| 121 |
+
# Adjectives
|
| 122 |
+
{"sign": "GOOD", "handshape": "FLAT", "location": "chin", "movement": "forward"},
|
| 123 |
+
{"sign": "BAD", "handshape": "FLAT", "location": "chin", "movement": "down"},
|
| 124 |
+
{"sign": "HAPPY", "handshape": "FLAT", "location": "chest", "movement": "circle_clockwise"},
|
| 125 |
+
{"sign": "SAD", "handshape": "OPEN", "location": "chin", "movement": "down"},
|
| 126 |
+
{"sign": "BIG", "handshape": "OPEN", "location": "neutral_space", "movement": "side_to_side"},
|
| 127 |
+
{"sign": "SMALL", "handshape": "FLAT", "location": "neutral_space", "movement": "tap"},
|
| 128 |
+
{"sign": "BEAUTIFUL", "handshape": "OPEN", "location": "chin", "movement": "circle_clockwise"},
|
| 129 |
+
{"sign": "EASY", "handshape": "FLAT", "location": "neutral_space", "movement": "up"},
|
| 130 |
+
{"sign": "HARD", "handshape": "V", "location": "neutral_space", "movement": "tap"},
|
| 131 |
+
{"sign": "HOT", "handshape": "CLAW", "location": "mouth", "movement": "forward"},
|
| 132 |
+
{"sign": "COLD", "handshape": "S", "location": "neutral_space", "movement": "side_to_side"},
|
| 133 |
+
{"sign": "NEW", "handshape": "FLAT", "location": "neutral_space", "movement": "forward"},
|
| 134 |
+
{"sign": "OLD", "handshape": "C", "location": "chin", "movement": "down"},
|
| 135 |
+
{"sign": "NICE", "handshape": "FLAT", "location": "neutral_space", "movement": "forward"},
|
| 136 |
+
{"sign": "FINE", "handshape": "OPEN", "location": "chest", "movement": "tap"},
|
| 137 |
+
{"sign": "DIFFERENT", "handshape": "POINT","location": "neutral_space", "movement": "side_to_side"},
|
| 138 |
+
{"sign": "SAME", "handshape": "POINT","location": "neutral_space", "movement": "tap"},
|
| 139 |
+
{"sign": "TRUE", "handshape": "POINT","location": "chin", "movement": "forward"},
|
| 140 |
+
{"sign": "WRONG", "handshape": "Y", "location": "chin", "movement": "tap"},
|
| 141 |
+
{"sign": "IMPORTANT", "handshape": "F", "location": "neutral_space", "movement": "up"},
|
| 142 |
+
{"sign": "READY", "handshape": "R", "location": "neutral_space", "movement": "side_to_side"},
|
| 143 |
+
{"sign": "DEAF", "handshape": "POINT","location": "ear", "movement": "forward"},
|
| 144 |
+
{"sign": "HUNGRY", "handshape": "C", "location": "chest", "movement": "down"},
|
| 145 |
+
|
| 146 |
+
# Adverbs / Misc
|
| 147 |
+
{"sign": "NOT", "handshape": "A", "location": "chin", "movement": "forward"},
|
| 148 |
+
{"sign": "NEVER", "handshape": "B", "location": "neutral_space", "movement": "down"},
|
| 149 |
+
{"sign": "ALWAYS", "handshape": "POINT","location": "neutral_space", "movement": "circle_clockwise"},
|
| 150 |
+
{"sign": "SOMETIMES", "handshape": "FLAT", "location": "neutral_space", "movement": "tap"},
|
| 151 |
+
{"sign": "AGAIN", "handshape": "FLAT", "location": "neutral_space", "movement": "tap"},
|
| 152 |
+
{"sign": "MORE", "handshape": "FLAT", "location": "neutral_space", "movement": "tap"},
|
| 153 |
+
{"sign": "ALSO", "handshape": "POINT","location": "neutral_space", "movement": "tap"},
|
| 154 |
+
{"sign": "NOW", "handshape": "FLAT", "location": "neutral_space", "movement": "down"},
|
| 155 |
+
{"sign": "LATER", "handshape": "L", "location": "neutral_space", "movement": "forward"},
|
| 156 |
+
{"sign": "HERE", "handshape": "FLAT", "location": "neutral_space", "movement": "circle_clockwise"},
|
| 157 |
+
{"sign": "THERE", "handshape": "POINT","location": "neutral_space", "movement": "forward"},
|
| 158 |
+
{"sign": "MAYBE", "handshape": "FLAT", "location": "neutral_space", "movement": "side_to_side"},
|
| 159 |
+
{"sign": "BECAUSE", "handshape": "POINT","location": "forehead", "movement": "forward"},
|
| 160 |
+
{"sign": "BUT", "handshape": "POINT","location": "neutral_space", "movement": "side_to_side"},
|
| 161 |
+
{"sign": "AND", "handshape": "OPEN", "location": "neutral_space", "movement": "forward"},
|
| 162 |
+
{"sign": "WITH", "handshape": "A", "location": "neutral_space", "movement": "tap"},
|
| 163 |
+
{"sign": "FOR", "handshape": "POINT","location": "forehead", "movement": "forward"},
|
| 164 |
+
{"sign": "FROM", "handshape": "X", "location": "neutral_space", "movement": "forward"},
|
| 165 |
+
{"sign": "ABOUT", "handshape": "POINT","location": "neutral_space", "movement": "circle_clockwise"},
|
| 166 |
+
{"sign": "MANY", "handshape": "S", "location": "neutral_space", "movement": "forward"},
|
| 167 |
+
{"sign": "ALL", "handshape": "OPEN", "location": "neutral_space", "movement": "circle_clockwise"},
|
| 168 |
+
{"sign": "EVERY", "handshape": "A", "location": "neutral_space", "movement": "down"},
|
| 169 |
+
{"sign": "ENOUGH", "handshape": "FLAT", "location": "neutral_space", "movement": "forward"},
|
| 170 |
+
]
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 174 |
+
# FINGERSPELLING
|
| 175 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 176 |
+
|
| 177 |
+
FINGERSPELLING = [
|
| 178 |
+
{"letter": "A", "handshape": "A", "movement": "none"},
|
| 179 |
+
{"letter": "B", "handshape": "B", "movement": "none"},
|
| 180 |
+
{"letter": "C", "handshape": "C", "movement": "none"},
|
| 181 |
+
{"letter": "D", "handshape": "D", "movement": "none"},
|
| 182 |
+
{"letter": "E", "handshape": "E", "movement": "none"},
|
| 183 |
+
{"letter": "F", "handshape": "F", "movement": "none"},
|
| 184 |
+
{"letter": "G", "handshape": "G", "movement": "none"},
|
| 185 |
+
{"letter": "H", "handshape": "H", "movement": "none"},
|
| 186 |
+
{"letter": "I", "handshape": "I", "movement": "none"},
|
| 187 |
+
{"letter": "J", "handshape": "J", "movement": "circle_clockwise"},
|
| 188 |
+
{"letter": "K", "handshape": "K", "movement": "none"},
|
| 189 |
+
{"letter": "L", "handshape": "L", "movement": "none"},
|
| 190 |
+
{"letter": "M", "handshape": "M", "movement": "none"},
|
| 191 |
+
{"letter": "N", "handshape": "N", "movement": "none"},
|
| 192 |
+
{"letter": "O", "handshape": "O", "movement": "none"},
|
| 193 |
+
{"letter": "P", "handshape": "P", "movement": "none"},
|
| 194 |
+
{"letter": "Q", "handshape": "Q", "movement": "none"},
|
| 195 |
+
{"letter": "R", "handshape": "R", "movement": "none"},
|
| 196 |
+
{"letter": "S", "handshape": "S", "movement": "none"},
|
| 197 |
+
{"letter": "T", "handshape": "T", "movement": "none"},
|
| 198 |
+
{"letter": "U", "handshape": "U", "movement": "none"},
|
| 199 |
+
{"letter": "V", "handshape": "V", "movement": "none"},
|
| 200 |
+
{"letter": "W", "handshape": "W", "movement": "none"},
|
| 201 |
+
{"letter": "X", "handshape": "X", "movement": "none"},
|
| 202 |
+
{"letter": "Y", "handshape": "Y", "movement": "none"},
|
| 203 |
+
{"letter": "Z", "handshape": "Z", "movement": "forward"},
|
| 204 |
+
]
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 208 |
+
# HANDSHAPES
|
| 209 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 210 |
+
|
| 211 |
+
HANDSHAPES = [
|
| 212 |
+
{"name": "A"},
|
| 213 |
+
{"name": "B"},
|
| 214 |
+
{"name": "C"},
|
| 215 |
+
{"name": "D"},
|
| 216 |
+
{"name": "E"},
|
| 217 |
+
{"name": "F"},
|
| 218 |
+
{"name": "G"},
|
| 219 |
+
{"name": "H"},
|
| 220 |
+
{"name": "I"},
|
| 221 |
+
{"name": "K"},
|
| 222 |
+
{"name": "L"},
|
| 223 |
+
{"name": "O"},
|
| 224 |
+
{"name": "P"},
|
| 225 |
+
{"name": "R"},
|
| 226 |
+
{"name": "S"},
|
| 227 |
+
{"name": "V"},
|
| 228 |
+
{"name": "W"},
|
| 229 |
+
{"name": "X"},
|
| 230 |
+
{"name": "Y"},
|
| 231 |
+
{"name": "OPEN"},
|
| 232 |
+
{"name": "FIST"},
|
| 233 |
+
{"name": "POINT"},
|
| 234 |
+
{"name": "FLAT"},
|
| 235 |
+
{"name": "CLAW"},
|
| 236 |
+
]
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 240 |
+
# LOCATIONS
|
| 241 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 242 |
+
|
| 243 |
+
LOCATIONS = [
|
| 244 |
+
{"name": "neutral_space"},
|
| 245 |
+
{"name": "chest"},
|
| 246 |
+
{"name": "chin"},
|
| 247 |
+
{"name": "mouth"},
|
| 248 |
+
{"name": "nose"},
|
| 249 |
+
{"name": "forehead"},
|
| 250 |
+
{"name": "temple"},
|
| 251 |
+
{"name": "side"},
|
| 252 |
+
{"name": "shoulder"},
|
| 253 |
+
{"name": "ear"},
|
| 254 |
+
{"name": "waist"},
|
| 255 |
+
]
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 259 |
+
# MOVEMENTS
|
| 260 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 261 |
+
|
| 262 |
+
MOVEMENTS = [
|
| 263 |
+
{"name": "none"},
|
| 264 |
+
{"name": "tap"},
|
| 265 |
+
{"name": "double_tap"},
|
| 266 |
+
{"name": "circle_clockwise"},
|
| 267 |
+
{"name": "circle_counterclockwise"},
|
| 268 |
+
{"name": "forward"},
|
| 269 |
+
{"name": "down"},
|
| 270 |
+
{"name": "up"},
|
| 271 |
+
{"name": "side_to_side"},
|
| 272 |
+
{"name": "nod"},
|
| 273 |
+
{"name": "twist"},
|
| 274 |
+
{"name": "wave"},
|
| 275 |
+
]
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def seed():
|
| 279 |
+
"""Seed all collections. Uses upsert to avoid duplicates."""
|
| 280 |
+
print("Seeding SignApp database...")
|
| 281 |
+
|
| 282 |
+
# Sign rules
|
| 283 |
+
col = db["sign_rules"]
|
| 284 |
+
for rule in SIGN_RULES:
|
| 285 |
+
col.update_one({"sign": rule["sign"]}, {"$set": rule}, upsert=True)
|
| 286 |
+
print(f" β sign_rules: {len(SIGN_RULES)} signs")
|
| 287 |
+
|
| 288 |
+
# Fingerspelling
|
| 289 |
+
col = db["fingerspelling"]
|
| 290 |
+
for fs in FINGERSPELLING:
|
| 291 |
+
col.update_one({"letter": fs["letter"]}, {"$set": fs}, upsert=True)
|
| 292 |
+
print(f" β fingerspelling: {len(FINGERSPELLING)} letters")
|
| 293 |
+
|
| 294 |
+
# Handshapes
|
| 295 |
+
col = db["handshapes"]
|
| 296 |
+
for hs in HANDSHAPES:
|
| 297 |
+
col.update_one({"name": hs["name"]}, {"$set": hs}, upsert=True)
|
| 298 |
+
print(f" β handshapes: {len(HANDSHAPES)} shapes")
|
| 299 |
+
|
| 300 |
+
# Locations
|
| 301 |
+
col = db["locations"]
|
| 302 |
+
for loc in LOCATIONS:
|
| 303 |
+
col.update_one({"name": loc["name"]}, {"$set": loc}, upsert=True)
|
| 304 |
+
print(f" β locations: {len(LOCATIONS)} locations")
|
| 305 |
+
|
| 306 |
+
# Movements
|
| 307 |
+
col = db["movements"]
|
| 308 |
+
for mov in MOVEMENTS:
|
| 309 |
+
col.update_one({"name": mov["name"]}, {"$set": mov}, upsert=True)
|
| 310 |
+
print(f" β movements: {len(MOVEMENTS)} movements")
|
| 311 |
+
|
| 312 |
+
print("\nβ
Database seeded successfully!")
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
if __name__ == "__main__":
|
| 316 |
+
seed()
|
src/sign_app/sign_language_text/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Conversion of text to sign-friendly format."""
|
src/sign_app/sign_language_text/gloss_converter.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hybrid English β ASL Gloss Converter
|
| 3 |
+
|
| 4 |
+
Combines three strategies for accurate gloss generation:
|
| 5 |
+
1. Rule-based grammar transforms (drop articles/copulas, reorder)
|
| 6 |
+
2. NLTK WordNet lemmatizer for verb/noun normalization
|
| 7 |
+
3. Comprehensive gloss lookup dictionary for idioms & common phrases
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import re
|
| 11 |
+
import nltk
|
| 12 |
+
from nltk.stem import WordNetLemmatizer
|
| 13 |
+
|
| 14 |
+
# Download required NLTK data (only once)
|
| 15 |
+
try:
|
| 16 |
+
nltk.data.find("corpora/wordnet")
|
| 17 |
+
except LookupError:
|
| 18 |
+
nltk.download("wordnet", quiet=True)
|
| 19 |
+
try:
|
| 20 |
+
nltk.data.find("taggers/averaged_perceptron_tagger_eng")
|
| 21 |
+
except LookupError:
|
| 22 |
+
nltk.download("averaged_perceptron_tagger_eng", quiet=True)
|
| 23 |
+
try:
|
| 24 |
+
nltk.data.find("corpora/omw-1.4")
|
| 25 |
+
except LookupError:
|
| 26 |
+
nltk.download("omw-1.4", quiet=True)
|
| 27 |
+
|
| 28 |
+
_lemmatizer = WordNetLemmatizer()
|
| 29 |
+
|
| 30 |
+
# ββ Words to drop (ASL omits these) ββββββββββββββββββββββββββββββββ
|
| 31 |
+
ARTICLES = {"a", "an", "the"}
|
| 32 |
+
COPULAS = {"is", "am", "are", "was", "were", "be", "been", "being"}
|
| 33 |
+
AUXILIARIES = {"do", "does", "did", "will", "would", "shall", "should",
|
| 34 |
+
"can", "could", "may", "might", "must", "has", "have", "had"}
|
| 35 |
+
PREPOSITIONS_DROP = {"to", "of"} # commonly dropped in ASL
|
| 36 |
+
FILLER_WORDS = {"just", "really", "very", "so", "um", "uh", "like",
|
| 37 |
+
"well", "actually", "basically", "literally"}
|
| 38 |
+
|
| 39 |
+
DROP_WORDS = ARTICLES | COPULAS | FILLER_WORDS
|
| 40 |
+
|
| 41 |
+
# ββ Phrase-level gloss dictionary (multi-word β single sign) βββββββ
|
| 42 |
+
PHRASE_GLOSSARY: dict[str, str] = {
|
| 43 |
+
"thank you": "THANK-YOU",
|
| 44 |
+
"thanks": "THANK-YOU",
|
| 45 |
+
"how are you": "HOW YOU",
|
| 46 |
+
"what's up": "WHAT-UP",
|
| 47 |
+
"good morning": "GOOD MORNING",
|
| 48 |
+
"good night": "GOOD NIGHT",
|
| 49 |
+
"good afternoon": "GOOD AFTERNOON",
|
| 50 |
+
"excuse me": "EXCUSE",
|
| 51 |
+
"i'm sorry": "SORRY",
|
| 52 |
+
"i am sorry": "SORRY",
|
| 53 |
+
"a lot": "MANY",
|
| 54 |
+
"don't": "NOT",
|
| 55 |
+
"doesn't": "NOT",
|
| 56 |
+
"didn't": "NOT",
|
| 57 |
+
"can't": "CAN NOT",
|
| 58 |
+
"cannot": "CAN NOT",
|
| 59 |
+
"won't": "WILL NOT",
|
| 60 |
+
"wouldn't": "WILL NOT",
|
| 61 |
+
"shouldn't": "SHOULD NOT",
|
| 62 |
+
"couldn't": "CAN NOT",
|
| 63 |
+
"isn't": "NOT",
|
| 64 |
+
"aren't": "NOT",
|
| 65 |
+
"wasn't": "NOT",
|
| 66 |
+
"weren't": "NOT",
|
| 67 |
+
"i'm": "I",
|
| 68 |
+
"i am": "I",
|
| 69 |
+
"you're": "YOU",
|
| 70 |
+
"you are": "YOU",
|
| 71 |
+
"he's": "HE",
|
| 72 |
+
"she's": "SHE",
|
| 73 |
+
"it's": "IT",
|
| 74 |
+
"we're": "WE",
|
| 75 |
+
"they're": "THEY",
|
| 76 |
+
"there is": "HAVE",
|
| 77 |
+
"there are": "HAVE",
|
| 78 |
+
"right now": "NOW",
|
| 79 |
+
"a little": "LITTLE",
|
| 80 |
+
"a bit": "LITTLE",
|
| 81 |
+
"of course": "OF-COURSE",
|
| 82 |
+
"no problem": "NO-PROBLEM",
|
| 83 |
+
"long time": "LONG-TIME",
|
| 84 |
+
"how much": "HOW-MUCH",
|
| 85 |
+
"how many": "HOW-MANY",
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
# ββ Word-level synonym/mapping dictionary ββββββββββββββββββββββββββ
|
| 89 |
+
WORD_GLOSSARY: dict[str, str] = {
|
| 90 |
+
# Greetings
|
| 91 |
+
"hello": "HELLO",
|
| 92 |
+
"hi": "HELLO",
|
| 93 |
+
"hey": "HELLO",
|
| 94 |
+
"goodbye": "BYE",
|
| 95 |
+
"bye": "BYE",
|
| 96 |
+
|
| 97 |
+
# Pronouns (pass-through but normalize)
|
| 98 |
+
"i": "I",
|
| 99 |
+
"me": "I",
|
| 100 |
+
"my": "MY",
|
| 101 |
+
"mine": "MY",
|
| 102 |
+
"you": "YOU",
|
| 103 |
+
"your": "YOUR",
|
| 104 |
+
"yours": "YOUR",
|
| 105 |
+
"he": "HE",
|
| 106 |
+
"him": "HE",
|
| 107 |
+
"his": "HIS",
|
| 108 |
+
"she": "SHE",
|
| 109 |
+
"her": "SHE",
|
| 110 |
+
"hers": "HER",
|
| 111 |
+
"it": "IT",
|
| 112 |
+
"its": "IT",
|
| 113 |
+
"we": "WE",
|
| 114 |
+
"us": "WE",
|
| 115 |
+
"our": "OUR",
|
| 116 |
+
"they": "THEY",
|
| 117 |
+
"them": "THEY",
|
| 118 |
+
"their": "THEIR",
|
| 119 |
+
|
| 120 |
+
# Common verbs (map to ASL base forms)
|
| 121 |
+
"want": "WANT",
|
| 122 |
+
"wants": "WANT",
|
| 123 |
+
"wanted": "WANT",
|
| 124 |
+
"wanting": "WANT",
|
| 125 |
+
"need": "NEED",
|
| 126 |
+
"needs": "NEED",
|
| 127 |
+
"needed": "NEED",
|
| 128 |
+
"like": "LIKE",
|
| 129 |
+
"likes": "LIKE",
|
| 130 |
+
"liked": "LIKE",
|
| 131 |
+
"love": "LOVE",
|
| 132 |
+
"loves": "LOVE",
|
| 133 |
+
"loved": "LOVE",
|
| 134 |
+
"know": "KNOW",
|
| 135 |
+
"knows": "KNOW",
|
| 136 |
+
"knew": "KNOW",
|
| 137 |
+
"known": "KNOW",
|
| 138 |
+
"think": "THINK",
|
| 139 |
+
"thinks": "THINK",
|
| 140 |
+
"thought": "THINK",
|
| 141 |
+
"see": "SEE",
|
| 142 |
+
"sees": "SEE",
|
| 143 |
+
"saw": "SEE",
|
| 144 |
+
"seen": "SEE",
|
| 145 |
+
"help": "HELP",
|
| 146 |
+
"helps": "HELP",
|
| 147 |
+
"helped": "HELP",
|
| 148 |
+
"go": "GO",
|
| 149 |
+
"goes": "GO",
|
| 150 |
+
"going": "GO",
|
| 151 |
+
"went": "GO",
|
| 152 |
+
"gone": "GO",
|
| 153 |
+
"come": "COME",
|
| 154 |
+
"comes": "COME",
|
| 155 |
+
"came": "COME",
|
| 156 |
+
"eat": "EAT",
|
| 157 |
+
"eats": "EAT",
|
| 158 |
+
"ate": "EAT",
|
| 159 |
+
"eaten": "EAT",
|
| 160 |
+
"drink": "DRINK",
|
| 161 |
+
"drinks": "DRINK",
|
| 162 |
+
"drank": "DRINK",
|
| 163 |
+
"work": "WORK",
|
| 164 |
+
"works": "WORK",
|
| 165 |
+
"worked": "WORK",
|
| 166 |
+
"working": "WORK",
|
| 167 |
+
"live": "LIVE",
|
| 168 |
+
"lives": "LIVE",
|
| 169 |
+
"lived": "LIVE",
|
| 170 |
+
"feel": "FEEL",
|
| 171 |
+
"feels": "FEEL",
|
| 172 |
+
"felt": "FEEL",
|
| 173 |
+
"say": "SAY",
|
| 174 |
+
"says": "SAY",
|
| 175 |
+
"said": "SAY",
|
| 176 |
+
"tell": "TELL",
|
| 177 |
+
"tells": "TELL",
|
| 178 |
+
"told": "TELL",
|
| 179 |
+
"ask": "ASK",
|
| 180 |
+
"asks": "ASK",
|
| 181 |
+
"asked": "ASK",
|
| 182 |
+
"give": "GIVE",
|
| 183 |
+
"gives": "GIVE",
|
| 184 |
+
"gave": "GIVE",
|
| 185 |
+
"given": "GIVE",
|
| 186 |
+
"take": "TAKE",
|
| 187 |
+
"takes": "TAKE",
|
| 188 |
+
"took": "TAKE",
|
| 189 |
+
"taken": "TAKE",
|
| 190 |
+
"make": "MAKE",
|
| 191 |
+
"makes": "MAKE",
|
| 192 |
+
"made": "MAKE",
|
| 193 |
+
"get": "GET",
|
| 194 |
+
"gets": "GET",
|
| 195 |
+
"got": "GET",
|
| 196 |
+
"wait": "WAIT",
|
| 197 |
+
"waits": "WAIT",
|
| 198 |
+
"waited": "WAIT",
|
| 199 |
+
"learn": "LEARN",
|
| 200 |
+
"learns": "LEARN",
|
| 201 |
+
"learned": "LEARN",
|
| 202 |
+
"teach": "TEACH",
|
| 203 |
+
"teaches": "TEACH",
|
| 204 |
+
"taught": "TEACH",
|
| 205 |
+
"understand": "UNDERSTAND",
|
| 206 |
+
"understands": "UNDERSTAND",
|
| 207 |
+
"understood": "UNDERSTAND",
|
| 208 |
+
"finish": "FINISH",
|
| 209 |
+
"finished": "FINISH",
|
| 210 |
+
"start": "START",
|
| 211 |
+
"started": "START",
|
| 212 |
+
"stop": "STOP",
|
| 213 |
+
"stopped": "STOP",
|
| 214 |
+
"try": "TRY",
|
| 215 |
+
"tries": "TRY",
|
| 216 |
+
"tried": "TRY",
|
| 217 |
+
"call": "CALL",
|
| 218 |
+
"called": "CALL",
|
| 219 |
+
"play": "PLAY",
|
| 220 |
+
"played": "PLAY",
|
| 221 |
+
"run": "RUN",
|
| 222 |
+
"ran": "RUN",
|
| 223 |
+
"walk": "WALK",
|
| 224 |
+
"walked": "WALK",
|
| 225 |
+
"sit": "SIT",
|
| 226 |
+
"sat": "SIT",
|
| 227 |
+
"stand": "STAND",
|
| 228 |
+
"stood": "STAND",
|
| 229 |
+
"open": "OPEN",
|
| 230 |
+
"opened": "OPEN",
|
| 231 |
+
"close": "CLOSE",
|
| 232 |
+
"closed": "CLOSE",
|
| 233 |
+
"buy": "BUY",
|
| 234 |
+
"bought": "BUY",
|
| 235 |
+
"bring": "BRING",
|
| 236 |
+
"brought": "BRING",
|
| 237 |
+
"read": "READ",
|
| 238 |
+
"write": "WRITE",
|
| 239 |
+
"wrote": "WRITE",
|
| 240 |
+
"written": "WRITE",
|
| 241 |
+
"speak": "SPEAK",
|
| 242 |
+
"spoke": "SPEAK",
|
| 243 |
+
"sign": "SIGN",
|
| 244 |
+
"signed": "SIGN",
|
| 245 |
+
"watch": "WATCH",
|
| 246 |
+
"look": "LOOK",
|
| 247 |
+
"looked": "LOOK",
|
| 248 |
+
"listen": "LISTEN",
|
| 249 |
+
|
| 250 |
+
# Question words
|
| 251 |
+
"what": "WHAT",
|
| 252 |
+
"where": "WHERE",
|
| 253 |
+
"when": "WHEN",
|
| 254 |
+
"why": "WHY",
|
| 255 |
+
"how": "HOW",
|
| 256 |
+
"who": "WHO",
|
| 257 |
+
"which": "WHICH",
|
| 258 |
+
|
| 259 |
+
# Common nouns
|
| 260 |
+
"person": "PERSON",
|
| 261 |
+
"people": "PEOPLE",
|
| 262 |
+
"man": "MAN",
|
| 263 |
+
"woman": "WOMAN",
|
| 264 |
+
"boy": "BOY",
|
| 265 |
+
"girl": "GIRL",
|
| 266 |
+
"child": "CHILD",
|
| 267 |
+
"children": "CHILD",
|
| 268 |
+
"baby": "BABY",
|
| 269 |
+
"friend": "FRIEND",
|
| 270 |
+
"friends": "FRIEND",
|
| 271 |
+
"family": "FAMILY",
|
| 272 |
+
"mother": "MOTHER",
|
| 273 |
+
"mom": "MOTHER",
|
| 274 |
+
"father": "FATHER",
|
| 275 |
+
"dad": "FATHER",
|
| 276 |
+
"brother": "BROTHER",
|
| 277 |
+
"sister": "SISTER",
|
| 278 |
+
"dog": "DOG",
|
| 279 |
+
"cat": "CAT",
|
| 280 |
+
"house": "HOUSE",
|
| 281 |
+
"home": "HOME",
|
| 282 |
+
"school": "SCHOOL",
|
| 283 |
+
"food": "FOOD",
|
| 284 |
+
"water": "WATER",
|
| 285 |
+
"car": "CAR",
|
| 286 |
+
"book": "BOOK",
|
| 287 |
+
"phone": "PHONE",
|
| 288 |
+
"name": "NAME",
|
| 289 |
+
"day": "DAY",
|
| 290 |
+
"today": "TODAY",
|
| 291 |
+
"tomorrow": "TOMORROW",
|
| 292 |
+
"yesterday": "YESTERDAY",
|
| 293 |
+
"morning": "MORNING",
|
| 294 |
+
"night": "NIGHT",
|
| 295 |
+
"time": "TIME",
|
| 296 |
+
"world": "WORLD",
|
| 297 |
+
"year": "YEAR",
|
| 298 |
+
"money": "MONEY",
|
| 299 |
+
"job": "WORK",
|
| 300 |
+
"store": "STORE",
|
| 301 |
+
"door": "DOOR",
|
| 302 |
+
"place": "PLACE",
|
| 303 |
+
"city": "CITY",
|
| 304 |
+
"country": "COUNTRY",
|
| 305 |
+
"weather": "WEATHER",
|
| 306 |
+
|
| 307 |
+
# Adjectives
|
| 308 |
+
"good": "GOOD",
|
| 309 |
+
"bad": "BAD",
|
| 310 |
+
"nice": "NICE",
|
| 311 |
+
"happy": "HAPPY",
|
| 312 |
+
"sad": "SAD",
|
| 313 |
+
"angry": "ANGRY",
|
| 314 |
+
"tired": "TIRED",
|
| 315 |
+
"sick": "SICK",
|
| 316 |
+
"big": "BIG",
|
| 317 |
+
"small": "SMALL",
|
| 318 |
+
"little": "SMALL",
|
| 319 |
+
"new": "NEW",
|
| 320 |
+
"old": "OLD",
|
| 321 |
+
"young": "YOUNG",
|
| 322 |
+
"beautiful": "BEAUTIFUL",
|
| 323 |
+
"pretty": "BEAUTIFUL",
|
| 324 |
+
"ugly": "UGLY",
|
| 325 |
+
"easy": "EASY",
|
| 326 |
+
"hard": "HARD",
|
| 327 |
+
"difficult": "HARD",
|
| 328 |
+
"fast": "FAST",
|
| 329 |
+
"quick": "FAST",
|
| 330 |
+
"slow": "SLOW",
|
| 331 |
+
"hot": "HOT",
|
| 332 |
+
"cold": "COLD",
|
| 333 |
+
"hungry": "HUNGRY",
|
| 334 |
+
"thirsty": "THIRSTY",
|
| 335 |
+
"important": "IMPORTANT",
|
| 336 |
+
"right": "RIGHT",
|
| 337 |
+
"wrong": "WRONG",
|
| 338 |
+
"same": "SAME",
|
| 339 |
+
"different": "DIFFERENT",
|
| 340 |
+
"ready": "READY",
|
| 341 |
+
"true": "TRUE",
|
| 342 |
+
"correct": "TRUE",
|
| 343 |
+
"deaf": "DEAF",
|
| 344 |
+
|
| 345 |
+
# Adverbs / misc
|
| 346 |
+
"yes": "YES",
|
| 347 |
+
"no": "NO",
|
| 348 |
+
"not": "NOT",
|
| 349 |
+
"never": "NEVER",
|
| 350 |
+
"always": "ALWAYS",
|
| 351 |
+
"sometimes": "SOMETIMES",
|
| 352 |
+
"often": "OFTEN",
|
| 353 |
+
"again": "AGAIN",
|
| 354 |
+
"more": "MORE",
|
| 355 |
+
"also": "ALSO",
|
| 356 |
+
"too": "ALSO",
|
| 357 |
+
"please": "PLEASE",
|
| 358 |
+
"sorry": "SORRY",
|
| 359 |
+
"now": "NOW",
|
| 360 |
+
"later": "LATER",
|
| 361 |
+
"here": "HERE",
|
| 362 |
+
"there": "THERE",
|
| 363 |
+
"maybe": "MAYBE",
|
| 364 |
+
"ok": "OK",
|
| 365 |
+
"okay": "OK",
|
| 366 |
+
"sure": "YES",
|
| 367 |
+
"fine": "FINE",
|
| 368 |
+
"together": "TOGETHER",
|
| 369 |
+
"before": "BEFORE",
|
| 370 |
+
"after": "AFTER",
|
| 371 |
+
"because": "BECAUSE",
|
| 372 |
+
"but": "BUT",
|
| 373 |
+
"and": "AND",
|
| 374 |
+
"or": "OR",
|
| 375 |
+
"if": "IF",
|
| 376 |
+
"then": "THEN",
|
| 377 |
+
"with": "WITH",
|
| 378 |
+
"for": "FOR",
|
| 379 |
+
"from": "FROM",
|
| 380 |
+
"in": "IN",
|
| 381 |
+
"at": "AT",
|
| 382 |
+
"on": "ON",
|
| 383 |
+
"about": "ABOUT",
|
| 384 |
+
"every": "EVERY",
|
| 385 |
+
"all": "ALL",
|
| 386 |
+
"many": "MANY",
|
| 387 |
+
"some": "SOME",
|
| 388 |
+
"enough": "ENOUGH",
|
| 389 |
+
"each": "EACH",
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
def _pos_tag_to_wordnet(tag: str) -> str:
|
| 394 |
+
"""Map NLTK POS tag to WordNet POS for the lemmatizer."""
|
| 395 |
+
if tag.startswith("V"):
|
| 396 |
+
return "v"
|
| 397 |
+
if tag.startswith("N"):
|
| 398 |
+
return "n"
|
| 399 |
+
if tag.startswith("J"):
|
| 400 |
+
return "a"
|
| 401 |
+
if tag.startswith("R"):
|
| 402 |
+
return "r"
|
| 403 |
+
return "n" # default noun
|
| 404 |
+
|
| 405 |
+
|
| 406 |
+
def convert_to_sign_gloss(text: str) -> list[str]:
|
| 407 |
+
"""
|
| 408 |
+
Convert English text to ASL gloss tokens.
|
| 409 |
+
|
| 410 |
+
Pipeline:
|
| 411 |
+
1. Lowercase & strip punctuation
|
| 412 |
+
2. Match multi-word phrases from PHRASE_GLOSSARY
|
| 413 |
+
3. For remaining words: lookup in WORD_GLOSSARY
|
| 414 |
+
4. If not in glossary: lemmatize with NLTK and try again
|
| 415 |
+
5. If still unknown: pass through as uppercase (will be fingerspelled)
|
| 416 |
+
6. Drop filler/grammar words that ASL omits
|
| 417 |
+
"""
|
| 418 |
+
# Normalize
|
| 419 |
+
text = text.lower().strip()
|
| 420 |
+
text = re.sub(r"[''']", "'", text) # normalize apostrophes
|
| 421 |
+
text = re.sub(r"[^\w\s'-]", " ", text) # strip punctuation except apostrophes/hyphens
|
| 422 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 423 |
+
|
| 424 |
+
# ββ Phase 1: Multi-word phrase matching ββββββββββββββββββββββββ
|
| 425 |
+
# Replace known phrases with their gloss (longest match first)
|
| 426 |
+
for phrase in sorted(PHRASE_GLOSSARY, key=len, reverse=True):
|
| 427 |
+
if phrase in text:
|
| 428 |
+
replacement = PHRASE_GLOSSARY[phrase]
|
| 429 |
+
text = text.replace(phrase, f" {replacement} ")
|
| 430 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 431 |
+
|
| 432 |
+
tokens = text.split()
|
| 433 |
+
|
| 434 |
+
# ββ Phase 2: Word-level processing βββββββββββββββββββββββββββββ
|
| 435 |
+
# POS-tag for better lemmatization
|
| 436 |
+
tagged = nltk.pos_tag(tokens)
|
| 437 |
+
|
| 438 |
+
gloss_tokens: list[str] = []
|
| 439 |
+
|
| 440 |
+
for word, tag in tagged:
|
| 441 |
+
# Already converted by phrase matching (uppercase)
|
| 442 |
+
if word.isupper() or "-" in word and word == word.upper():
|
| 443 |
+
# Split multi-token gloss results
|
| 444 |
+
for part in word.split():
|
| 445 |
+
gloss_tokens.append(part)
|
| 446 |
+
continue
|
| 447 |
+
|
| 448 |
+
# Skip drop words
|
| 449 |
+
if word in DROP_WORDS:
|
| 450 |
+
continue
|
| 451 |
+
|
| 452 |
+
# Skip auxiliaries (ASL mostly drops these)
|
| 453 |
+
if word in AUXILIARIES:
|
| 454 |
+
continue
|
| 455 |
+
|
| 456 |
+
# Skip prepositions that ASL drops
|
| 457 |
+
if word in PREPOSITIONS_DROP:
|
| 458 |
+
continue
|
| 459 |
+
|
| 460 |
+
# Direct glossary lookup
|
| 461 |
+
if word in WORD_GLOSSARY:
|
| 462 |
+
gloss_tokens.append(WORD_GLOSSARY[word])
|
| 463 |
+
continue
|
| 464 |
+
|
| 465 |
+
# Try lemmatization then glossary
|
| 466 |
+
wn_pos = _pos_tag_to_wordnet(tag)
|
| 467 |
+
lemma = _lemmatizer.lemmatize(word, pos=wn_pos)
|
| 468 |
+
|
| 469 |
+
if lemma in WORD_GLOSSARY:
|
| 470 |
+
gloss_tokens.append(WORD_GLOSSARY[lemma])
|
| 471 |
+
continue
|
| 472 |
+
|
| 473 |
+
# Try verb lemmatization specifically (catches "going" -> "go")
|
| 474 |
+
verb_lemma = _lemmatizer.lemmatize(word, pos="v")
|
| 475 |
+
if verb_lemma in WORD_GLOSSARY:
|
| 476 |
+
gloss_tokens.append(WORD_GLOSSARY[verb_lemma])
|
| 477 |
+
continue
|
| 478 |
+
|
| 479 |
+
# Unknown word β pass through uppercase (will be fingerspelled)
|
| 480 |
+
gloss_tokens.append(word.upper())
|
| 481 |
+
|
| 482 |
+
return gloss_tokens
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
# ββ Test βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 486 |
+
if __name__ == "__main__":
|
| 487 |
+
tests = [
|
| 488 |
+
"Hello, I know you are a good person.",
|
| 489 |
+
"I want to go to the store",
|
| 490 |
+
"She is going to the park later",
|
| 491 |
+
"Can you help me with this?",
|
| 492 |
+
"The weather is nice today",
|
| 493 |
+
"Thank you for your help",
|
| 494 |
+
"I don't understand",
|
| 495 |
+
"What is your name?",
|
| 496 |
+
"How are you?",
|
| 497 |
+
"I'm sorry, I can't come tomorrow",
|
| 498 |
+
]
|
| 499 |
+
for t in tests:
|
| 500 |
+
result = convert_to_sign_gloss(t)
|
| 501 |
+
print(f"{t:45s} β {' '.join(result)}")
|
src/sign_app/sign_language_text/inference.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import torch
|
| 3 |
+
from transformers import T5ForConditionalGeneration, T5Tokenizer
|
| 4 |
+
|
| 5 |
+
MODEL_PATH = "./sign_language_converter_model"
|
| 6 |
+
|
| 7 |
+
# Load tokenizer & model
|
| 8 |
+
tokenizer = T5Tokenizer.from_pretrained(MODEL_PATH)
|
| 9 |
+
model = T5ForConditionalGeneration.from_pretrained(MODEL_PATH)
|
| 10 |
+
|
| 11 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 12 |
+
model.to(device)
|
| 13 |
+
model.eval()
|
| 14 |
+
|
| 15 |
+
def convert_to_sign_friendly(text: str) -> list[str]:
|
| 16 |
+
inputs = tokenizer(
|
| 17 |
+
"convert to sign-friendly: " + text,
|
| 18 |
+
return_tensors="pt",
|
| 19 |
+
truncation=True,
|
| 20 |
+
padding=True
|
| 21 |
+
).to(device)
|
| 22 |
+
|
| 23 |
+
with torch.no_grad():
|
| 24 |
+
outputs = model.generate(
|
| 25 |
+
**inputs,
|
| 26 |
+
max_length=256,
|
| 27 |
+
num_beams=4,
|
| 28 |
+
early_stopping=True
|
| 29 |
+
)
|
| 30 |
+
sign_friendly_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 31 |
+
sign_friendly_text = clean_gloss(sign_friendly_text)
|
| 32 |
+
|
| 33 |
+
return sign_friendly_text
|
| 34 |
+
|
| 35 |
+
def clean_gloss(gloss: str) -> list[str]:
|
| 36 |
+
words = gloss.split()
|
| 37 |
+
|
| 38 |
+
cleaned = []
|
| 39 |
+
for w in words:
|
| 40 |
+
|
| 41 |
+
w = re.sub(r"[,.!]", "", w)
|
| 42 |
+
|
| 43 |
+
if w.startswith("X-"):
|
| 44 |
+
w = w.replace("X-", "")
|
| 45 |
+
if w.startswith("DESC-"):
|
| 46 |
+
w = w.replace("DESC-", "")
|
| 47 |
+
|
| 48 |
+
cleaned.append(w)
|
| 49 |
+
|
| 50 |
+
return cleaned
|
| 51 |
+
|
| 52 |
+
# Test the conversion on some example sentences
|
| 53 |
+
if __name__ == "__main__":
|
| 54 |
+
text = "I want to go to the store"
|
| 55 |
+
print(convert_to_sign_friendly(text))
|
| 56 |
+
print(convert_to_sign_friendly("She is going to the park later"))
|
| 57 |
+
print(convert_to_sign_friendly("Can you help me with this?"))
|
| 58 |
+
print(convert_to_sign_friendly("The weather is nice today"))
|
src/sign_app/sign_language_text/training.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datasets import load_dataset
|
| 2 |
+
from transformers import T5Tokenizer, T5ForConditionalGeneration, Seq2SeqTrainer, Seq2SeqTrainingArguments, DataCollatorForSeq2Seq
|
| 3 |
+
import mlflow
|
| 4 |
+
import evaluate
|
| 5 |
+
import nltk
|
| 6 |
+
|
| 7 |
+
nltk.download('punkt')
|
| 8 |
+
bleu = evaluate.load("bleu")
|
| 9 |
+
rouge = evaluate.load("rouge")
|
| 10 |
+
|
| 11 |
+
base_model = "t5-small"
|
| 12 |
+
|
| 13 |
+
tokenizer = T5Tokenizer.from_pretrained(base_model)
|
| 14 |
+
transformer_model = T5ForConditionalGeneration.from_pretrained(base_model)
|
| 15 |
+
|
| 16 |
+
sign_language_conversion_dataset = load_dataset("achrafothman/aslg_pc12")
|
| 17 |
+
|
| 18 |
+
sign_language_conversion_dataset = load_dataset("achrafothman/aslg_pc12")
|
| 19 |
+
|
| 20 |
+
# Split 90% train, 10% validation
|
| 21 |
+
sign_language_conversion_dataset = sign_language_conversion_dataset["train"].train_test_split(test_size=0.1)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def sign_friendly_mapping(example):
|
| 25 |
+
return {
|
| 26 |
+
"input_text": example["text"],
|
| 27 |
+
"target_text": example["gloss"]
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
Dataset = sign_language_conversion_dataset.map(
|
| 31 |
+
sign_friendly_mapping,
|
| 32 |
+
remove_columns=sign_language_conversion_dataset["train"].column_names
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
def is_valid(example):
|
| 36 |
+
return (
|
| 37 |
+
example["input_text"] is not None
|
| 38 |
+
and example["target_text"] is not None
|
| 39 |
+
and example["input_text"].strip() != ""
|
| 40 |
+
and example["target_text"].strip() != ""
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
Dataset = Dataset.filter(is_valid)
|
| 44 |
+
|
| 45 |
+
max_input_length = 256
|
| 46 |
+
max_target_length = 256
|
| 47 |
+
|
| 48 |
+
def tokenize_function(examples):
|
| 49 |
+
inputs = ["convert to sign-friendly: " + text for text in examples["input_text"]]
|
| 50 |
+
model_inputs = tokenizer(inputs, max_length=max_input_length, truncation=True, padding="max_length")
|
| 51 |
+
labels = tokenizer(
|
| 52 |
+
examples["target_text"],
|
| 53 |
+
max_length=max_target_length,
|
| 54 |
+
truncation=True,
|
| 55 |
+
padding="max_length"
|
| 56 |
+
)
|
| 57 |
+
model_inputs["labels"] = labels["input_ids"]
|
| 58 |
+
return model_inputs
|
| 59 |
+
|
| 60 |
+
tokenized_dataset = Dataset.map(
|
| 61 |
+
tokenize_function, batched=True, remove_columns=Dataset["train"].column_names
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
data_collator = DataCollatorForSeq2Seq(tokenizer, model=transformer_model)
|
| 65 |
+
|
| 66 |
+
def compute_metrics(eval_pred):
|
| 67 |
+
predictions, labels = eval_pred
|
| 68 |
+
decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True)
|
| 69 |
+
labels = [[label if label != -100 else tokenizer.pad_token_id for label in l] for l in labels]
|
| 70 |
+
decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)
|
| 71 |
+
|
| 72 |
+
decoded_preds = [pred.strip() for pred in decoded_preds]
|
| 73 |
+
decoded_labels = [label.strip() for label in decoded_labels]
|
| 74 |
+
|
| 75 |
+
bleu_result = bleu.compute(predictions=decoded_preds, references=decoded_labels)
|
| 76 |
+
rouge_result = rouge.compute(predictions=decoded_preds, references=decoded_labels)
|
| 77 |
+
|
| 78 |
+
return {
|
| 79 |
+
"bleu": bleu_result["bleu"],
|
| 80 |
+
"rouge1": rouge_result["rouge1"],
|
| 81 |
+
"rouge2": rouge_result["rouge2"],
|
| 82 |
+
"rougeL": rouge_result["rougeL"]
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
training_args = Seq2SeqTrainingArguments(
|
| 86 |
+
output_dir="./sign_language_converter_model",
|
| 87 |
+
eval_strategy="epoch",
|
| 88 |
+
save_strategy="epoch",
|
| 89 |
+
learning_rate=3e-5,
|
| 90 |
+
per_device_train_batch_size=8,
|
| 91 |
+
per_device_eval_batch_size=8,
|
| 92 |
+
num_train_epochs=5,
|
| 93 |
+
weight_decay=0.01,
|
| 94 |
+
predict_with_generate=True,
|
| 95 |
+
save_total_limit=2,
|
| 96 |
+
logging_steps=100,
|
| 97 |
+
fp16=True, # set to True if using a GPU with mixed precision support
|
| 98 |
+
report_to="mlflow",
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
trainer = Seq2SeqTrainer(
|
| 102 |
+
model=transformer_model,
|
| 103 |
+
args=training_args,
|
| 104 |
+
train_dataset=tokenized_dataset["train"],
|
| 105 |
+
eval_dataset=tokenized_dataset["test"],
|
| 106 |
+
data_collator=data_collator,
|
| 107 |
+
compute_metrics=compute_metrics
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
mlflow.set_tracking_uri("./mlruns")
|
| 111 |
+
mlflow.set_experiment("Sign Language Text Conversion")
|
| 112 |
+
with mlflow.start_run(run_name="T5 Sign Language Converter"):
|
| 113 |
+
trainer.train()
|
| 114 |
+
trainer.save_model("./sign_language_converter_model")
|
| 115 |
+
tokenizer.save_pretrained("./sign_language_converter_model")
|
| 116 |
+
|
| 117 |
+
# Test the trained model on some example sentences
|
| 118 |
+
def convert_to_sign_friendly(text: str) -> str:
|
| 119 |
+
inputs = tokenizer("convert to sign-friendly: " + text, return_tensors="pt", truncation=True).input_ids.to(transformer_model.device)
|
| 120 |
+
outputs = transformer_model.generate(inputs, max_length=max_target_length, num_beams=4, early_stopping=True)
|
| 121 |
+
sign_friendly_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 122 |
+
return sign_friendly_text.strip()
|
| 123 |
+
|
| 124 |
+
if __name__ == "__main__":
|
| 125 |
+
test_sentence = "I want to go to the store"
|
| 126 |
+
print("Original:", test_sentence)
|
| 127 |
+
print("Sign-friendly:", convert_to_sign_friendly(test_sentence))
|
src/sign_app/ui/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""UI and API interfaces module."""
|
src/sign_app/ui/avatar.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c22dc6f68103100395277bf585d51b7b8d1c509d103f510ff05fb5538f63745a
|
| 3 |
+
size 935628
|
src/sign_app/ui/fingerspellDictionary.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* ASL Fingerspelling Dictionary
|
| 3 |
+
*
|
| 4 |
+
* Each letter maps to rotations for ALL finger bones (3 knuckles each)
|
| 5 |
+
* plus thumb (3 joints) and optional wrist rotation.
|
| 6 |
+
*
|
| 7 |
+
* Values are in radians:
|
| 8 |
+
* 0 = straight / open
|
| 9 |
+
* 1.2 = fully curled
|
| 10 |
+
* 0.6 = half curled
|
| 11 |
+
*
|
| 12 |
+
* Thumb uses both X (curl) and Z (spread/opposition) axes.
|
| 13 |
+
*/
|
| 14 |
+
export const FINGERSPELL = {
|
| 15 |
+
|
| 16 |
+
A: {
|
| 17 |
+
thumb1: { x: 0.2, y: 0, z: 0.3 },
|
| 18 |
+
thumb2: { x: 0.1, y: 0, z: 0 },
|
| 19 |
+
thumb3: { x: 0, y: 0, z: 0 },
|
| 20 |
+
index1: { x: 1.2, y: 0, z: 0 }, index2: { x: 1.2, y: 0, z: 0 }, index3: { x: 1.0, y: 0, z: 0 },
|
| 21 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.2, y: 0, z: 0 }, middle3: { x: 1.0, y: 0, z: 0 },
|
| 22 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.2, y: 0, z: 0 }, ring3: { x: 1.0, y: 0, z: 0 },
|
| 23 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.2, y: 0, z: 0 }, pinky3: { x: 1.0, y: 0, z: 0 },
|
| 24 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 25 |
+
},
|
| 26 |
+
|
| 27 |
+
B: {
|
| 28 |
+
thumb1: { x: 0.8, y: 0, z: 0.4 },
|
| 29 |
+
thumb2: { x: 0.6, y: 0, z: 0 },
|
| 30 |
+
thumb3: { x: 0.3, y: 0, z: 0 },
|
| 31 |
+
index1: { x: 0, y: 0, z: 0 }, index2: { x: 0, y: 0, z: 0 }, index3: { x: 0, y: 0, z: 0 },
|
| 32 |
+
middle1: { x: 0, y: 0, z: 0 }, middle2: { x: 0, y: 0, z: 0 }, middle3: { x: 0, y: 0, z: 0 },
|
| 33 |
+
ring1: { x: 0, y: 0, z: 0 }, ring2: { x: 0, y: 0, z: 0 }, ring3: { x: 0, y: 0, z: 0 },
|
| 34 |
+
pinky1: { x: 0, y: 0, z: 0 }, pinky2: { x: 0, y: 0, z: 0 }, pinky3: { x: 0, y: 0, z: 0 },
|
| 35 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 36 |
+
},
|
| 37 |
+
|
| 38 |
+
C: {
|
| 39 |
+
thumb1: { x: 0.3, y: 0, z: 0.2 },
|
| 40 |
+
thumb2: { x: 0.3, y: 0, z: 0 },
|
| 41 |
+
thumb3: { x: 0.2, y: 0, z: 0 },
|
| 42 |
+
index1: { x: 0.5, y: 0, z: 0 }, index2: { x: 0.4, y: 0, z: 0 }, index3: { x: 0.3, y: 0, z: 0 },
|
| 43 |
+
middle1: { x: 0.5, y: 0, z: 0 }, middle2: { x: 0.4, y: 0, z: 0 }, middle3: { x: 0.3, y: 0, z: 0 },
|
| 44 |
+
ring1: { x: 0.5, y: 0, z: 0 }, ring2: { x: 0.4, y: 0, z: 0 }, ring3: { x: 0.3, y: 0, z: 0 },
|
| 45 |
+
pinky1: { x: 0.5, y: 0, z: 0 }, pinky2: { x: 0.4, y: 0, z: 0 }, pinky3: { x: 0.3, y: 0, z: 0 },
|
| 46 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 47 |
+
},
|
| 48 |
+
|
| 49 |
+
D: {
|
| 50 |
+
thumb1: { x: 0.6, y: 0, z: 0.4 },
|
| 51 |
+
thumb2: { x: 0.5, y: 0, z: 0 },
|
| 52 |
+
thumb3: { x: 0.3, y: 0, z: 0 },
|
| 53 |
+
index1: { x: 0, y: 0, z: 0 }, index2: { x: 0, y: 0, z: 0 }, index3: { x: 0, y: 0, z: 0 },
|
| 54 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.0, y: 0, z: 0 }, middle3: { x: 0.8, y: 0, z: 0 },
|
| 55 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 56 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 57 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 58 |
+
},
|
| 59 |
+
|
| 60 |
+
E: {
|
| 61 |
+
thumb1: { x: 0.3, y: 0, z: 0.3 },
|
| 62 |
+
thumb2: { x: 0.2, y: 0, z: 0 },
|
| 63 |
+
thumb3: { x: 0.1, y: 0, z: 0 },
|
| 64 |
+
index1: { x: 1.0, y: 0, z: 0 }, index2: { x: 0.8, y: 0, z: 0 }, index3: { x: 0.6, y: 0, z: 0 },
|
| 65 |
+
middle1: { x: 1.0, y: 0, z: 0 }, middle2: { x: 0.8, y: 0, z: 0 }, middle3: { x: 0.6, y: 0, z: 0 },
|
| 66 |
+
ring1: { x: 1.0, y: 0, z: 0 }, ring2: { x: 0.8, y: 0, z: 0 }, ring3: { x: 0.6, y: 0, z: 0 },
|
| 67 |
+
pinky1: { x: 1.0, y: 0, z: 0 }, pinky2: { x: 0.8, y: 0, z: 0 }, pinky3: { x: 0.6, y: 0, z: 0 },
|
| 68 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 69 |
+
},
|
| 70 |
+
|
| 71 |
+
F: {
|
| 72 |
+
thumb1: { x: 0.6, y: 0, z: 0.3 },
|
| 73 |
+
thumb2: { x: 0.5, y: 0, z: 0 },
|
| 74 |
+
thumb3: { x: 0.3, y: 0, z: 0 },
|
| 75 |
+
index1: { x: 0.8, y: 0, z: 0 }, index2: { x: 0.6, y: 0, z: 0 }, index3: { x: 0.4, y: 0, z: 0 },
|
| 76 |
+
middle1: { x: 0, y: 0, z: 0 }, middle2: { x: 0, y: 0, z: 0 }, middle3: { x: 0, y: 0, z: 0 },
|
| 77 |
+
ring1: { x: 0, y: 0, z: 0 }, ring2: { x: 0, y: 0, z: 0 }, ring3: { x: 0, y: 0, z: 0 },
|
| 78 |
+
pinky1: { x: 0, y: 0, z: 0 }, pinky2: { x: 0, y: 0, z: 0 }, pinky3: { x: 0, y: 0, z: 0 },
|
| 79 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 80 |
+
},
|
| 81 |
+
|
| 82 |
+
G: {
|
| 83 |
+
thumb1: { x: 0.2, y: 0, z: -0.4 },
|
| 84 |
+
thumb2: { x: 0.1, y: 0, z: 0 },
|
| 85 |
+
thumb3: { x: 0, y: 0, z: 0 },
|
| 86 |
+
index1: { x: 0, y: 0, z: 0 }, index2: { x: 0, y: 0, z: 0 }, index3: { x: 0, y: 0, z: 0 },
|
| 87 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.0, y: 0, z: 0 }, middle3: { x: 0.8, y: 0, z: 0 },
|
| 88 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 89 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 90 |
+
wrist: { x: 0, y: -0.5, z: 0 }
|
| 91 |
+
},
|
| 92 |
+
|
| 93 |
+
H: {
|
| 94 |
+
thumb1: { x: 0.3, y: 0, z: 0.3 },
|
| 95 |
+
thumb2: { x: 0.2, y: 0, z: 0 },
|
| 96 |
+
thumb3: { x: 0.1, y: 0, z: 0 },
|
| 97 |
+
index1: { x: 0, y: 0, z: 0 }, index2: { x: 0, y: 0, z: 0 }, index3: { x: 0, y: 0, z: 0 },
|
| 98 |
+
middle1: { x: 0, y: 0, z: 0 }, middle2: { x: 0, y: 0, z: 0 }, middle3: { x: 0, y: 0, z: 0 },
|
| 99 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 100 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 101 |
+
wrist: { x: 0, y: -0.5, z: 0 }
|
| 102 |
+
},
|
| 103 |
+
|
| 104 |
+
I: {
|
| 105 |
+
thumb1: { x: 0.6, y: 0, z: 0.3 },
|
| 106 |
+
thumb2: { x: 0.5, y: 0, z: 0 },
|
| 107 |
+
thumb3: { x: 0.3, y: 0, z: 0 },
|
| 108 |
+
index1: { x: 1.2, y: 0, z: 0 }, index2: { x: 1.0, y: 0, z: 0 }, index3: { x: 0.8, y: 0, z: 0 },
|
| 109 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.0, y: 0, z: 0 }, middle3: { x: 0.8, y: 0, z: 0 },
|
| 110 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 111 |
+
pinky1: { x: 0, y: 0, z: 0 }, pinky2: { x: 0, y: 0, z: 0 }, pinky3: { x: 0, y: 0, z: 0 },
|
| 112 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 113 |
+
},
|
| 114 |
+
|
| 115 |
+
J: {
|
| 116 |
+
thumb1: { x: 0.6, y: 0, z: 0.3 },
|
| 117 |
+
thumb2: { x: 0.5, y: 0, z: 0 },
|
| 118 |
+
thumb3: { x: 0.3, y: 0, z: 0 },
|
| 119 |
+
index1: { x: 1.2, y: 0, z: 0 }, index2: { x: 1.0, y: 0, z: 0 }, index3: { x: 0.8, y: 0, z: 0 },
|
| 120 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.0, y: 0, z: 0 }, middle3: { x: 0.8, y: 0, z: 0 },
|
| 121 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 122 |
+
pinky1: { x: 0, y: 0, z: 0 }, pinky2: { x: 0, y: 0, z: 0 }, pinky3: { x: 0, y: 0, z: 0 },
|
| 123 |
+
wrist: { x: 0, y: 0.6, z: 0 } // J = I + wrist hook
|
| 124 |
+
},
|
| 125 |
+
|
| 126 |
+
K: {
|
| 127 |
+
thumb1: { x: 0.3, y: 0, z: 0.2 },
|
| 128 |
+
thumb2: { x: 0.2, y: 0, z: 0 },
|
| 129 |
+
thumb3: { x: 0.1, y: 0, z: 0 },
|
| 130 |
+
index1: { x: 0, y: 0, z: 0 }, index2: { x: 0, y: 0, z: 0 }, index3: { x: 0, y: 0, z: 0 },
|
| 131 |
+
middle1: { x: 0, y: 0, z: 0 }, middle2: { x: 0, y: 0, z: 0 }, middle3: { x: 0, y: 0, z: 0 },
|
| 132 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 133 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 134 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 135 |
+
},
|
| 136 |
+
|
| 137 |
+
L: {
|
| 138 |
+
thumb1: { x: 0, y: 0, z: -0.6 },
|
| 139 |
+
thumb2: { x: 0, y: 0, z: 0 },
|
| 140 |
+
thumb3: { x: 0, y: 0, z: 0 },
|
| 141 |
+
index1: { x: 0, y: 0, z: 0 }, index2: { x: 0, y: 0, z: 0 }, index3: { x: 0, y: 0, z: 0 },
|
| 142 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.0, y: 0, z: 0 }, middle3: { x: 0.8, y: 0, z: 0 },
|
| 143 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 144 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 145 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 146 |
+
},
|
| 147 |
+
|
| 148 |
+
M: {
|
| 149 |
+
thumb1: { x: 0.6, y: 0, z: 0.4 },
|
| 150 |
+
thumb2: { x: 0.5, y: 0, z: 0 },
|
| 151 |
+
thumb3: { x: 0.3, y: 0, z: 0 },
|
| 152 |
+
index1: { x: 1.2, y: 0, z: 0 }, index2: { x: 1.0, y: 0, z: 0 }, index3: { x: 0.8, y: 0, z: 0 },
|
| 153 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.0, y: 0, z: 0 }, middle3: { x: 0.8, y: 0, z: 0 },
|
| 154 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 155 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 156 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 157 |
+
},
|
| 158 |
+
|
| 159 |
+
N: {
|
| 160 |
+
thumb1: { x: 0.6, y: 0, z: 0.4 },
|
| 161 |
+
thumb2: { x: 0.5, y: 0, z: 0 },
|
| 162 |
+
thumb3: { x: 0.3, y: 0, z: 0 },
|
| 163 |
+
index1: { x: 1.2, y: 0, z: 0 }, index2: { x: 1.0, y: 0, z: 0 }, index3: { x: 0.8, y: 0, z: 0 },
|
| 164 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.0, y: 0, z: 0 }, middle3: { x: 0.8, y: 0, z: 0 },
|
| 165 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 166 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 167 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 168 |
+
},
|
| 169 |
+
|
| 170 |
+
O: {
|
| 171 |
+
thumb1: { x: 0.4, y: 0, z: 0.3 },
|
| 172 |
+
thumb2: { x: 0.3, y: 0, z: 0 },
|
| 173 |
+
thumb3: { x: 0.2, y: 0, z: 0 },
|
| 174 |
+
index1: { x: 0.6, y: 0, z: 0 }, index2: { x: 0.5, y: 0, z: 0 }, index3: { x: 0.4, y: 0, z: 0 },
|
| 175 |
+
middle1: { x: 0.6, y: 0, z: 0 }, middle2: { x: 0.5, y: 0, z: 0 }, middle3: { x: 0.4, y: 0, z: 0 },
|
| 176 |
+
ring1: { x: 0.6, y: 0, z: 0 }, ring2: { x: 0.5, y: 0, z: 0 }, ring3: { x: 0.4, y: 0, z: 0 },
|
| 177 |
+
pinky1: { x: 0.6, y: 0, z: 0 }, pinky2: { x: 0.5, y: 0, z: 0 }, pinky3: { x: 0.4, y: 0, z: 0 },
|
| 178 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 179 |
+
},
|
| 180 |
+
|
| 181 |
+
P: {
|
| 182 |
+
thumb1: { x: 0.2, y: 0, z: -0.3 },
|
| 183 |
+
thumb2: { x: 0.1, y: 0, z: 0 },
|
| 184 |
+
thumb3: { x: 0, y: 0, z: 0 },
|
| 185 |
+
index1: { x: 0, y: 0, z: 0 }, index2: { x: 0, y: 0, z: 0 }, index3: { x: 0, y: 0, z: 0 },
|
| 186 |
+
middle1: { x: 0.4, y: 0, z: 0 }, middle2: { x: 0.3, y: 0, z: 0 }, middle3: { x: 0.2, y: 0, z: 0 },
|
| 187 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 188 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 189 |
+
wrist: { x: 0.5, y: 0, z: 0 } // hand tilted down
|
| 190 |
+
},
|
| 191 |
+
|
| 192 |
+
Q: {
|
| 193 |
+
thumb1: { x: 0.3, y: 0, z: -0.3 },
|
| 194 |
+
thumb2: { x: 0.2, y: 0, z: 0 },
|
| 195 |
+
thumb3: { x: 0.1, y: 0, z: 0 },
|
| 196 |
+
index1: { x: 0.4, y: 0, z: 0 }, index2: { x: 0.3, y: 0, z: 0 }, index3: { x: 0.2, y: 0, z: 0 },
|
| 197 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.0, y: 0, z: 0 }, middle3: { x: 0.8, y: 0, z: 0 },
|
| 198 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 199 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 200 |
+
wrist: { x: 0.5, y: 0, z: 0 }
|
| 201 |
+
},
|
| 202 |
+
|
| 203 |
+
R: {
|
| 204 |
+
thumb1: { x: 0.6, y: 0, z: 0.3 },
|
| 205 |
+
thumb2: { x: 0.5, y: 0, z: 0 },
|
| 206 |
+
thumb3: { x: 0.3, y: 0, z: 0 },
|
| 207 |
+
index1: { x: 0, y: 0, z: -0.1 }, index2: { x: 0, y: 0, z: 0 }, index3: { x: 0, y: 0, z: 0 },
|
| 208 |
+
middle1: { x: 0.1, y: 0, z: 0.2 }, middle2: { x: 0.2, y: 0, z: 0 }, middle3: { x: 0.1, y: 0, z: 0 },
|
| 209 |
+
ring1: { x: 1.5, y: 0, z: 0 }, ring2: { x: 1.2, y: 0, z: 0 }, ring3: { x: 1.0, y: 0, z: 0 },
|
| 210 |
+
pinky1: { x: 1.5, y: 0, z: 0 }, pinky2: { x: 1.2, y: 0, z: 0 }, pinky3: { x: 1.0, y: 0, z: 0 },
|
| 211 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 212 |
+
},
|
| 213 |
+
|
| 214 |
+
S: {
|
| 215 |
+
thumb1: { x: 0.3, y: 0, z: 0.3 },
|
| 216 |
+
thumb2: { x: 0.2, y: 0, z: 0 },
|
| 217 |
+
thumb3: { x: 0.1, y: 0, z: 0 },
|
| 218 |
+
index1: { x: 1.2, y: 0, z: 0 }, index2: { x: 1.2, y: 0, z: 0 }, index3: { x: 1.0, y: 0, z: 0 },
|
| 219 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.2, y: 0, z: 0 }, middle3: { x: 1.0, y: 0, z: 0 },
|
| 220 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.2, y: 0, z: 0 }, ring3: { x: 1.0, y: 0, z: 0 },
|
| 221 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.2, y: 0, z: 0 }, pinky3: { x: 1.0, y: 0, z: 0 },
|
| 222 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 223 |
+
},
|
| 224 |
+
|
| 225 |
+
T: {
|
| 226 |
+
thumb1: { x: 0.2, y: 0, z: 0.2 },
|
| 227 |
+
thumb2: { x: 0.2, y: 0, z: 0 },
|
| 228 |
+
thumb3: { x: 0.1, y: 0, z: 0 },
|
| 229 |
+
index1: { x: 1.2, y: 0, z: 0 }, index2: { x: 1.0, y: 0, z: 0 }, index3: { x: 0.8, y: 0, z: 0 },
|
| 230 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.0, y: 0, z: 0 }, middle3: { x: 0.8, y: 0, z: 0 },
|
| 231 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 232 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 233 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 234 |
+
},
|
| 235 |
+
|
| 236 |
+
U: {
|
| 237 |
+
thumb1: { x: 0.6, y: 0, z: 0.3 },
|
| 238 |
+
thumb2: { x: 0.5, y: 0, z: 0 },
|
| 239 |
+
thumb3: { x: 0.3, y: 0, z: 0 },
|
| 240 |
+
index1: { x: 0, y: 0, z: 0 }, index2: { x: 0, y: 0, z: 0 }, index3: { x: 0, y: 0, z: 0 },
|
| 241 |
+
middle1: { x: 0, y: 0, z: 0 }, middle2: { x: 0, y: 0, z: 0 }, middle3: { x: 0, y: 0, z: 0 },
|
| 242 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 243 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 244 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 245 |
+
},
|
| 246 |
+
|
| 247 |
+
V: {
|
| 248 |
+
thumb1: { x: 0.6, y: 0, z: 0.3 },
|
| 249 |
+
thumb2: { x: 0.5, y: 0, z: 0 },
|
| 250 |
+
thumb3: { x: 0.3, y: 0, z: 0 },
|
| 251 |
+
index1: { x: 0, y: 0, z: -0.15 }, index2: { x: 0, y: 0, z: 0 }, index3: { x: 0, y: 0, z: 0 },
|
| 252 |
+
middle1: { x: 0, y: 0, z: 0.15 }, middle2: { x: 0, y: 0, z: 0 }, middle3: { x: 0, y: 0, z: 0 },
|
| 253 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 254 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 255 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 256 |
+
},
|
| 257 |
+
|
| 258 |
+
W: {
|
| 259 |
+
thumb1: { x: 0.6, y: 0, z: 0.4 },
|
| 260 |
+
thumb2: { x: 0.5, y: 0, z: 0 },
|
| 261 |
+
thumb3: { x: 0.3, y: 0, z: 0 },
|
| 262 |
+
index1: { x: 0, y: 0, z: -0.15 }, index2: { x: 0, y: 0, z: 0 }, index3: { x: 0, y: 0, z: 0 },
|
| 263 |
+
middle1: { x: 0, y: 0, z: 0 }, middle2: { x: 0, y: 0, z: 0 }, middle3: { x: 0, y: 0, z: 0 },
|
| 264 |
+
ring1: { x: 0, y: 0, z: 0.15 }, ring2: { x: 0, y: 0, z: 0 }, ring3: { x: 0, y: 0, z: 0 },
|
| 265 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 266 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 267 |
+
},
|
| 268 |
+
|
| 269 |
+
X: {
|
| 270 |
+
thumb1: { x: 0.6, y: 0, z: 0.3 },
|
| 271 |
+
thumb2: { x: 0.5, y: 0, z: 0 },
|
| 272 |
+
thumb3: { x: 0.3, y: 0, z: 0 },
|
| 273 |
+
index1: { x: 0.6, y: 0, z: 0 }, index2: { x: 0.8, y: 0, z: 0 }, index3: { x: 0.6, y: 0, z: 0 },
|
| 274 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.0, y: 0, z: 0 }, middle3: { x: 0.8, y: 0, z: 0 },
|
| 275 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 276 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 277 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 278 |
+
},
|
| 279 |
+
|
| 280 |
+
Y: {
|
| 281 |
+
thumb1: { x: 0, y: 0, z: -0.5 },
|
| 282 |
+
thumb2: { x: 0, y: 0, z: 0 },
|
| 283 |
+
thumb3: { x: 0, y: 0, z: 0 },
|
| 284 |
+
index1: { x: 1.2, y: 0, z: 0 }, index2: { x: 1.0, y: 0, z: 0 }, index3: { x: 0.8, y: 0, z: 0 },
|
| 285 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.0, y: 0, z: 0 }, middle3: { x: 0.8, y: 0, z: 0 },
|
| 286 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 287 |
+
pinky1: { x: 0, y: 0, z: 0 }, pinky2: { x: 0, y: 0, z: 0 }, pinky3: { x: 0, y: 0, z: 0 },
|
| 288 |
+
wrist: { x: 0, y: 0, z: 0 }
|
| 289 |
+
},
|
| 290 |
+
|
| 291 |
+
Z: {
|
| 292 |
+
thumb1: { x: 0.6, y: 0, z: 0.3 },
|
| 293 |
+
thumb2: { x: 0.5, y: 0, z: 0 },
|
| 294 |
+
thumb3: { x: 0.3, y: 0, z: 0 },
|
| 295 |
+
index1: { x: 0, y: 0, z: 0 }, index2: { x: 0, y: 0, z: 0 }, index3: { x: 0, y: 0, z: 0 },
|
| 296 |
+
middle1: { x: 1.2, y: 0, z: 0 }, middle2: { x: 1.0, y: 0, z: 0 }, middle3: { x: 0.8, y: 0, z: 0 },
|
| 297 |
+
ring1: { x: 1.2, y: 0, z: 0 }, ring2: { x: 1.0, y: 0, z: 0 }, ring3: { x: 0.8, y: 0, z: 0 },
|
| 298 |
+
pinky1: { x: 1.2, y: 0, z: 0 }, pinky2: { x: 1.0, y: 0, z: 0 }, pinky3: { x: 0.8, y: 0, z: 0 },
|
| 299 |
+
wrist: { x: 0, y: 0.4, z: 0 } // Z = index point + wrist trace
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/**
|
| 304 |
+
* Maps short key names to actual avatar bone names
|
| 305 |
+
*/
|
| 306 |
+
export const FINGER_BONE_MAP = {
|
| 307 |
+
thumb1: "RightHandThumb1",
|
| 308 |
+
thumb2: "RightHandThumb2",
|
| 309 |
+
thumb3: "RightHandThumb3",
|
| 310 |
+
index1: "RightHandIndex1",
|
| 311 |
+
index2: "RightHandIndex2",
|
| 312 |
+
index3: "RightHandIndex3",
|
| 313 |
+
middle1: "RightHandMiddle1",
|
| 314 |
+
middle2: "RightHandMiddle2",
|
| 315 |
+
middle3: "RightHandMiddle3",
|
| 316 |
+
ring1: "RightHandRing1",
|
| 317 |
+
ring2: "RightHandRing2",
|
| 318 |
+
ring3: "RightHandRing3",
|
| 319 |
+
pinky1: "RightHandPinky1",
|
| 320 |
+
pinky2: "RightHandPinky2",
|
| 321 |
+
pinky3: "RightHandPinky3",
|
| 322 |
+
wrist: "RightHand"
|
| 323 |
+
}
|
src/sign_app/ui/index.html
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>SignApp - Speech to Sign Language</title>
|
| 8 |
+
<meta name="description" content="Real-time speech to sign language avatar powered by AI">
|
| 9 |
+
|
| 10 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 12 |
+
|
| 13 |
+
<script type="importmap">
|
| 14 |
+
{
|
| 15 |
+
"imports": {
|
| 16 |
+
"three": "https://unpkg.com/three@0.158.0/build/three.module.js",
|
| 17 |
+
"three/addons/": "https://unpkg.com/three@0.158.0/examples/jsm/"
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
</script>
|
| 21 |
+
|
| 22 |
+
<style>
|
| 23 |
+
|
| 24 |
+
:root {
|
| 25 |
+
--bg-color-top: #0c0a1a;
|
| 26 |
+
--bg-color-mid: #1a1333;
|
| 27 |
+
--bg-color-bot: #0d1b2a;
|
| 28 |
+
--text-main: #e2e8f0;
|
| 29 |
+
--text-main-muted: #cbd5e1;
|
| 30 |
+
--text-muted: #64748b;
|
| 31 |
+
--topbar-bg: rgba(12,10,26,0.9);
|
| 32 |
+
--bottom-panel-bg1: rgba(12,10,26,0.85);
|
| 33 |
+
--bottom-panel-bg2: rgba(12,10,26,0.4);
|
| 34 |
+
--chip-bg: rgba(255,255,255,0.06);
|
| 35 |
+
--chip-border: rgba(255,255,255,0.08);
|
| 36 |
+
--input-bg: rgba(255,255,255,0.05);
|
| 37 |
+
--input-border: rgba(255,255,255,0.1);
|
| 38 |
+
--input-focus: rgba(167,139,250,0.4);
|
| 39 |
+
--accent-color: #a78bfa;
|
| 40 |
+
--accent-hover: rgba(167,139,250,0.3);
|
| 41 |
+
--btn-bg: rgba(167,139,250,0.2);
|
| 42 |
+
--btn-border: rgba(167,139,250,0.3);
|
| 43 |
+
--btn-text: #c4b5fd;
|
| 44 |
+
--btn-hover-bg: rgba(167,139,250,0.3);
|
| 45 |
+
--btn-hover-text: #fff;
|
| 46 |
+
--btn-sec-bg: rgba(255,255,255,0.05);
|
| 47 |
+
--btn-sec-border: rgba(255,255,255,0.1);
|
| 48 |
+
--btn-sec-hover-bg: rgba(255,255,255,0.1);
|
| 49 |
+
--btn-sec-hover-border: rgba(255,255,255,0.2);
|
| 50 |
+
--btn-sec-text: #94a3b8;
|
| 51 |
+
--btn-sec-hover-text: #fff;
|
| 52 |
+
--status-dot-processing: #f59e0b;
|
| 53 |
+
--gloss-token-bg: rgba(167,139,250,0.15);
|
| 54 |
+
--gloss-token-border: rgba(167,139,250,0.25);
|
| 55 |
+
--gloss-token-text: #c4b5fd;
|
| 56 |
+
--gloss-token-active-bg: rgba(167,139,250,0.35);
|
| 57 |
+
--gloss-token-active-border: #a78bfa;
|
| 58 |
+
--gloss-token-active-text: #fff;
|
| 59 |
+
}
|
| 60 |
+
.light-mode {
|
| 61 |
+
--bg-color-top: #f8fafc;
|
| 62 |
+
--bg-color-mid: #e2e8f0;
|
| 63 |
+
--bg-color-bot: #cbd5e1;
|
| 64 |
+
--text-main: #1e293b;
|
| 65 |
+
--text-main-muted: #334155;
|
| 66 |
+
--text-muted: #475569;
|
| 67 |
+
--topbar-bg: rgba(248, 250, 252, 0.9);
|
| 68 |
+
--bottom-panel-bg1: rgba(248, 250, 252, 0.95);
|
| 69 |
+
--bottom-panel-bg2: rgba(248, 250, 252, 0.6);
|
| 70 |
+
--chip-bg: rgba(0,0,0,0.05);
|
| 71 |
+
--chip-border: rgba(0,0,0,0.1);
|
| 72 |
+
--input-bg: rgba(0,0,0,0.05);
|
| 73 |
+
--input-border: rgba(0,0,0,0.1);
|
| 74 |
+
--input-focus: rgba(124,58,237,0.4);
|
| 75 |
+
--accent-color: #7c3aed;
|
| 76 |
+
--accent-hover: rgba(124, 58, 237, 0.15);
|
| 77 |
+
--btn-bg: rgba(124, 58, 237, 0.1);
|
| 78 |
+
--btn-border: rgba(124, 58, 237, 0.2);
|
| 79 |
+
--btn-text: #6d28d9;
|
| 80 |
+
--btn-hover-bg: rgba(124, 58, 237, 0.2);
|
| 81 |
+
--btn-hover-text: #1e293b;
|
| 82 |
+
--btn-sec-bg: rgba(0,0,0,0.05);
|
| 83 |
+
--btn-sec-border: rgba(0,0,0,0.1);
|
| 84 |
+
--btn-sec-hover-bg: rgba(0,0,0,0.1);
|
| 85 |
+
--btn-sec-hover-border: rgba(0,0,0,0.2);
|
| 86 |
+
--btn-sec-text: #475569;
|
| 87 |
+
--btn-sec-hover-text: #1e293b;
|
| 88 |
+
--status-dot-processing: #d97706;
|
| 89 |
+
--gloss-token-bg: rgba(124, 58, 237, 0.1);
|
| 90 |
+
--gloss-token-border: rgba(124, 58, 237, 0.2);
|
| 91 |
+
--gloss-token-text: #6d28d9;
|
| 92 |
+
--gloss-token-active-bg: rgba(124, 58, 237, 0.25);
|
| 93 |
+
--gloss-token-active-border: #7c3aed;
|
| 94 |
+
--gloss-token-active-text: #1e293b;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
| 98 |
+
|
| 99 |
+
body {
|
| 100 |
+
font-family: 'Inter', sans-serif;
|
| 101 |
+
background: linear-gradient(135deg, var(--bg-color-top) 0%, var(--bg-color-mid) 40%, var(--bg-color-bot) 100%);
|
| 102 |
+
color: var(--text-main);
|
| 103 |
+
overflow: hidden;
|
| 104 |
+
height: 100vh;
|
| 105 |
+
width: 100vw;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/* ββ 3D Canvas βββββββββββββββββββββββββββββββ */
|
| 109 |
+
#avatar-canvas {
|
| 110 |
+
position: fixed;
|
| 111 |
+
top: 0; left: 0;
|
| 112 |
+
width: 100%; height: 100%;
|
| 113 |
+
z-index: 0;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/* ββ Top Bar βββββββββββββββββββββββββββββββββ */
|
| 117 |
+
#top-bar {
|
| 118 |
+
position: fixed;
|
| 119 |
+
top: 0; left: 0; right: 0;
|
| 120 |
+
z-index: 20;
|
| 121 |
+
display: flex;
|
| 122 |
+
align-items: center;
|
| 123 |
+
justify-content: space-between;
|
| 124 |
+
padding: 16px 28px;
|
| 125 |
+
background: linear-gradient(180deg, var(--topbar-bg) 0%, transparent 100%);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.logo {
|
| 129 |
+
font-weight: 700;
|
| 130 |
+
font-size: 20px;
|
| 131 |
+
letter-spacing: -0.5px;
|
| 132 |
+
background: linear-gradient(135deg, #a78bfa, #60a5fa);
|
| 133 |
+
-webkit-background-clip: text;
|
| 134 |
+
background-clip: text;
|
| 135 |
+
-webkit-text-fill-color: transparent;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.status-chip {
|
| 139 |
+
display: flex;
|
| 140 |
+
align-items: center;
|
| 141 |
+
gap: 8px;
|
| 142 |
+
padding: 6px 14px;
|
| 143 |
+
border-radius: 20px;
|
| 144 |
+
font-size: 13px;
|
| 145 |
+
font-weight: 500;
|
| 146 |
+
background: var(--chip-bg);
|
| 147 |
+
border: 1px solid var(--chip-border);
|
| 148 |
+
backdrop-filter: blur(10px);
|
| 149 |
+
transition: all 0.3s ease;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.status-dot {
|
| 153 |
+
width: 8px; height: 8px;
|
| 154 |
+
border-radius: 50%;
|
| 155 |
+
background: #4ade80;
|
| 156 |
+
transition: background 0.3s;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.status-dot.recording {
|
| 160 |
+
background: #ef4444;
|
| 161 |
+
animation: pulse-dot 1s ease-in-out infinite;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.status-dot.processing {
|
| 165 |
+
background: var(--status-dot-processing);
|
| 166 |
+
animation: pulse-dot 0.6s ease-in-out infinite;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.status-dot.signing {
|
| 170 |
+
background: var(--accent-color);
|
| 171 |
+
animation: pulse-dot 1.2s ease-in-out infinite;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
@keyframes pulse-dot {
|
| 175 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 176 |
+
50% { opacity: 0.5; transform: scale(1.3); }
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/* ββ Bottom Panel ββββββββββββββββββββββββββββ */
|
| 180 |
+
#bottom-panel {
|
| 181 |
+
position: fixed;
|
| 182 |
+
bottom: 0; left: 0; right: 0;
|
| 183 |
+
z-index: 20;
|
| 184 |
+
display: flex;
|
| 185 |
+
flex-direction: column;
|
| 186 |
+
align-items: center;
|
| 187 |
+
gap: 16px;
|
| 188 |
+
padding: 20px 28px 32px;
|
| 189 |
+
background: linear-gradient(0deg, var(--bottom-panel-bg1) 0%, var(--bottom-panel-bg2) 70%, transparent 100%);
|
| 190 |
+
pointer-events: none;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
#bottom-panel > * {
|
| 194 |
+
pointer-events: auto;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
/* ββ Transcript Area βββββββββββββββββββββββββ */
|
| 199 |
+
#transcript-area {
|
| 200 |
+
width: 100%;
|
| 201 |
+
max-width: 640px;
|
| 202 |
+
text-align: center;
|
| 203 |
+
min-height: 50px;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.transcript-label {
|
| 207 |
+
font-size: 11px;
|
| 208 |
+
text-transform: uppercase;
|
| 209 |
+
letter-spacing: 1.5px;
|
| 210 |
+
color: var(--text-muted);
|
| 211 |
+
margin-bottom: 6px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.transcript-text {
|
| 215 |
+
font-size: 15px;
|
| 216 |
+
color: var(--text-main-muted);
|
| 217 |
+
line-height: 1.5;
|
| 218 |
+
opacity: 0;
|
| 219 |
+
transform: translateY(8px);
|
| 220 |
+
transition: all 0.4s ease;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.transcript-text.visible {
|
| 224 |
+
opacity: 1;
|
| 225 |
+
transform: translateY(0);
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.gloss-tokens {
|
| 229 |
+
display: flex;
|
| 230 |
+
flex-wrap: wrap;
|
| 231 |
+
gap: 6px;
|
| 232 |
+
justify-content: center;
|
| 233 |
+
margin-top: 8px;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.gloss-token {
|
| 237 |
+
padding: 4px 10px;
|
| 238 |
+
border-radius: 6px;
|
| 239 |
+
font-size: 13px;
|
| 240 |
+
font-weight: 600;
|
| 241 |
+
font-family: 'Inter', monospace;
|
| 242 |
+
background: var(--gloss-token-bg);
|
| 243 |
+
border: 1px solid var(--gloss-token-border);
|
| 244 |
+
color: var(--gloss-token-text);
|
| 245 |
+
transition: all 0.3s ease;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.gloss-token.active {
|
| 249 |
+
background: var(--gloss-token-active-bg);
|
| 250 |
+
border-color: var(--gloss-token-active-border);
|
| 251 |
+
color: #fff;
|
| 252 |
+
transform: scale(1.08);
|
| 253 |
+
box-shadow: 0 0 12px rgba(167,139,250,0.3);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.gloss-token.done {
|
| 257 |
+
opacity: 0.4;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
/* ββ Mic Button ββββββββββββββββββββββββββββββ */
|
| 261 |
+
#mic-btn {
|
| 262 |
+
width: 72px; height: 72px;
|
| 263 |
+
border-radius: 50%;
|
| 264 |
+
border: none;
|
| 265 |
+
cursor: pointer;
|
| 266 |
+
display: flex;
|
| 267 |
+
align-items: center;
|
| 268 |
+
justify-content: center;
|
| 269 |
+
background: linear-gradient(135deg, #7c3aed, #3b82f6);
|
| 270 |
+
box-shadow: 0 4px 24px rgba(124,58,237,0.4), 0 0 0 0 rgba(124,58,237,0);
|
| 271 |
+
transition: all 0.3s ease;
|
| 272 |
+
position: relative;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
#mic-btn:hover {
|
| 276 |
+
transform: scale(1.06);
|
| 277 |
+
box-shadow: 0 6px 32px rgba(124,58,237,0.5);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
#mic-btn:active { transform: scale(0.97); }
|
| 281 |
+
|
| 282 |
+
#mic-btn.recording {
|
| 283 |
+
background: linear-gradient(135deg, #ef4444, #dc2626);
|
| 284 |
+
box-shadow: 0 4px 24px rgba(239,68,68,0.4);
|
| 285 |
+
animation: pulse-ring 1.5s ease-out infinite;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
#mic-btn.processing {
|
| 289 |
+
background: linear-gradient(135deg, #f59e0b, #d97706);
|
| 290 |
+
pointer-events: none;
|
| 291 |
+
animation: spin-slow 2s linear infinite;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
@keyframes pulse-ring {
|
| 295 |
+
0% { box-shadow: 0 4px 24px rgba(239,68,68,0.4), 0 0 0 0 rgba(239,68,68,0.3); }
|
| 296 |
+
100% { box-shadow: 0 4px 24px rgba(239,68,68,0.4), 0 0 0 20px rgba(239,68,68,0); }
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
@keyframes spin-slow {
|
| 300 |
+
from { transform: rotate(0deg); }
|
| 301 |
+
to { transform: rotate(360deg); }
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.mic-icon {
|
| 305 |
+
width: 28px; height: 28px;
|
| 306 |
+
fill: white;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
/* ββ Audio Waveform Visualizer ββββββββββββββββ */
|
| 310 |
+
#waveform {
|
| 311 |
+
width: 200px;
|
| 312 |
+
height: 40px;
|
| 313 |
+
display: flex;
|
| 314 |
+
align-items: center;
|
| 315 |
+
justify-content: center;
|
| 316 |
+
gap: 3px;
|
| 317 |
+
opacity: 0;
|
| 318 |
+
transition: opacity 0.3s;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
#waveform.active { opacity: 1; }
|
| 322 |
+
|
| 323 |
+
.wave-bar {
|
| 324 |
+
width: 3px;
|
| 325 |
+
height: 8px;
|
| 326 |
+
border-radius: 2px;
|
| 327 |
+
background: var(--accent-color);
|
| 328 |
+
transition: height 0.1s ease;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
/* ββ Signing Progress ββββββββββββββββββββββββ */
|
| 332 |
+
#sign-label {
|
| 333 |
+
font-size: 13px;
|
| 334 |
+
color: var(--text-muted);
|
| 335 |
+
font-weight: 500;
|
| 336 |
+
opacity: 0;
|
| 337 |
+
transition: opacity 0.3s;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
#sign-label.visible { opacity: 1; }
|
| 341 |
+
|
| 342 |
+
/* ββ Text Input Area ββββββββββββββββββββββββββ */
|
| 343 |
+
#text-input-container {
|
| 344 |
+
width: 100%;
|
| 345 |
+
max-width: 500px;
|
| 346 |
+
display: flex;
|
| 347 |
+
gap: 10px;
|
| 348 |
+
padding: 4px;
|
| 349 |
+
background: var(--input-bg);
|
| 350 |
+
border: 1px solid var(--input-border);
|
| 351 |
+
border-radius: 12px;
|
| 352 |
+
margin-bottom: 8px;
|
| 353 |
+
transition: all 0.3s ease;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
#text-input-container:focus-within {
|
| 357 |
+
background: var(--input-bg);
|
| 358 |
+
border-color: var(--input-focus);
|
| 359 |
+
box-shadow: 0 0 15px var(--accent-hover);
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
#text-input {
|
| 363 |
+
color: var(--text-main);
|
| 364 |
+
flex: 1;
|
| 365 |
+
background: transparent;
|
| 366 |
+
border: none;
|
| 367 |
+
outline: none;
|
| 368 |
+
color: #fff;
|
| 369 |
+
padding: 10px 16px;
|
| 370 |
+
font-family: inherit;
|
| 371 |
+
font-size: 14px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
#text-input::placeholder { color: var(--text-muted); }
|
| 375 |
+
|
| 376 |
+
#send-btn {
|
| 377 |
+
background: var(--btn-bg);
|
| 378 |
+
border: 1px solid var(--btn-border);
|
| 379 |
+
color: var(--gloss-token-text);
|
| 380 |
+
padding: 0 16px;
|
| 381 |
+
border-radius: 8px;
|
| 382 |
+
cursor: pointer;
|
| 383 |
+
font-size: 13px;
|
| 384 |
+
font-weight: 600;
|
| 385 |
+
transition: all 0.2s ease;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
#send-btn:hover {
|
| 389 |
+
background: var(--btn-hover-bg);
|
| 390 |
+
color: #fff;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
/* ββ Controls Layout ββββββββββββββββββββββββββ */
|
| 394 |
+
.controls-row {
|
| 395 |
+
display: flex;
|
| 396 |
+
align-items: center;
|
| 397 |
+
gap: 20px;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
/* ββ Secondary Button (Replay) ββββββββββββββββ */
|
| 401 |
+
.secondary-btn {
|
| 402 |
+
width: 48px; height: 48px;
|
| 403 |
+
border-radius: 50%;
|
| 404 |
+
border: 1px solid var(--input-border);
|
| 405 |
+
background: var(--input-bg);
|
| 406 |
+
cursor: pointer;
|
| 407 |
+
display: flex;
|
| 408 |
+
align-items: center;
|
| 409 |
+
justify-content: center;
|
| 410 |
+
transition: all 0.3s ease;
|
| 411 |
+
color: var(--text-muted);
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
.secondary-btn:hover {
|
| 415 |
+
background: rgba(255,255,255,0.1);
|
| 416 |
+
border-color: rgba(255,255,255,0.2);
|
| 417 |
+
color: #fff;
|
| 418 |
+
transform: translateY(-2px);
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.secondary-btn:active { transform: translateY(0); }
|
| 422 |
+
|
| 423 |
+
.secondary-btn:disabled {
|
| 424 |
+
opacity: 0.3;
|
| 425 |
+
cursor: not-allowed;
|
| 426 |
+
transform: none;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.btn-icon { width: 20px; height: 20px; fill: currentColor; }
|
| 430 |
+
|
| 431 |
+
/* ββ Responsive adjustments βββββββββββββββββββ */
|
| 432 |
+
@media (max-width: 600px), (max-height: 700px) {
|
| 433 |
+
.transcript-text { font-size: 13px; line-height: 1.3; }
|
| 434 |
+
.gloss-token { font-size: 11px; padding: 3px 8px; }
|
| 435 |
+
#bottom-panel { padding: 12px 16px 20px; gap: 8px; }
|
| 436 |
+
#text-input-container { max-width: 100%; margin-bottom: 4px; }
|
| 437 |
+
#mic-btn { width: 60px; height: 60px; }
|
| 438 |
+
#waveform { height: 30px; }
|
| 439 |
+
}
|
| 440 |
+
</style>
|
| 441 |
+
</head>
|
| 442 |
+
|
| 443 |
+
<body>
|
| 444 |
+
|
| 445 |
+
<!-- Top Bar -->
|
| 446 |
+
<div id="top-bar">
|
| 447 |
+
|
| 448 |
+
<div style="display: flex; gap: 16px; align-items: center;">
|
| 449 |
+
<div class="logo">SignApp</div>
|
| 450 |
+
<button id="theme-btn" class="secondary-btn" style="width: 36px; height: 36px;" title="Toggle Theme">
|
| 451 |
+
<svg class="btn-icon" viewBox="0 0 24 24" id="theme-icon" style="width: 18px; height: 18px;">
|
| 452 |
+
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41.39.39 1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41.39.39 1.03.39 1.41 0l1.06-1.06z"/>
|
| 453 |
+
</svg>
|
| 454 |
+
</button>
|
| 455 |
+
</div>
|
| 456 |
+
|
| 457 |
+
<div class="status-chip">
|
| 458 |
+
<span class="status-dot" id="status-dot"></span>
|
| 459 |
+
<span id="status-text">Ready</span>
|
| 460 |
+
</div>
|
| 461 |
+
</div>
|
| 462 |
+
|
| 463 |
+
<!-- Bottom Panel -->
|
| 464 |
+
<div id="bottom-panel">
|
| 465 |
+
|
| 466 |
+
<!-- Transcript -->
|
| 467 |
+
<div id="transcript-area">
|
| 468 |
+
<div class="transcript-label" id="transcript-label"></div>
|
| 469 |
+
<div class="transcript-text" id="transcript-text"></div>
|
| 470 |
+
<div class="gloss-tokens" id="gloss-tokens"></div>
|
| 471 |
+
</div>
|
| 472 |
+
|
| 473 |
+
<!-- Text Input -->
|
| 474 |
+
<div id="text-input-container">
|
| 475 |
+
<input type="text" id="text-input" placeholder="Or type something here..." autocomplete="off">
|
| 476 |
+
<button id="send-btn">Send</button>
|
| 477 |
+
</div>
|
| 478 |
+
|
| 479 |
+
<!-- Waveform -->
|
| 480 |
+
<div id="waveform"></div>
|
| 481 |
+
|
| 482 |
+
<!-- Sign Progress -->
|
| 483 |
+
<div id="sign-label"></div>
|
| 484 |
+
<div style="font-size: 10px; color: var(--text-muted); opacity: 0.8; text-align: center; max-width: 80%; margin-top: -8px; margin-bottom: 4px;">Note: Generated signs are AI approximations and may not be 100% accurate.</div>
|
| 485 |
+
|
| 486 |
+
<!-- Controls -->
|
| 487 |
+
<div class="controls-row">
|
| 488 |
+
<!-- Replay Button -->
|
| 489 |
+
<button id="replay-btn" class="secondary-btn" title="Replay last signs" disabled>
|
| 490 |
+
<svg class="btn-icon" viewBox="0 0 24 24">
|
| 491 |
+
<path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>
|
| 492 |
+
</svg>
|
| 493 |
+
</button>
|
| 494 |
+
|
| 495 |
+
<!-- Microphone Button -->
|
| 496 |
+
<button id="mic-btn" title="Click to speak">
|
| 497 |
+
<svg class="mic-icon" viewBox="0 0 24 24" id="mic-svg">
|
| 498 |
+
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5z"/>
|
| 499 |
+
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
|
| 500 |
+
</svg>
|
| 501 |
+
</button>
|
| 502 |
+
|
| 503 |
+
<!-- Placeholder to balance the row if needed, or other future buttons -->
|
| 504 |
+
<div style="width: 48px;"></div>
|
| 505 |
+
</div>
|
| 506 |
+
</div>
|
| 507 |
+
|
| 508 |
+
<script type="module" src="./main.js"></script>
|
| 509 |
+
|
| 510 |
+
</body>
|
| 511 |
+
</html>
|
src/sign_app/ui/main.js
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as THREE from "three"
|
| 2 |
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"
|
| 3 |
+
import { playSentence, setCallbacks } from "./signEngine.js"
|
| 4 |
+
import { FINGERSPELL, FINGER_BONE_MAP } from "./fingerspellDictionary.js"
|
| 5 |
+
|
| 6 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
+
GLOBALS
|
| 8 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 9 |
+
|
| 10 |
+
window.targetRotations = {}
|
| 11 |
+
window.signing = false
|
| 12 |
+
|
| 13 |
+
let idleTime = 0
|
| 14 |
+
let baseRotations = {}
|
| 15 |
+
let bones = {}
|
| 16 |
+
let avatar
|
| 17 |
+
|
| 18 |
+
/* βββ UI Elements ββββββββββββββββββββββββββββββββββββββββ */
|
| 19 |
+
const statusDot = document.getElementById("status-dot")
|
| 20 |
+
const statusText = document.getElementById("status-text")
|
| 21 |
+
const micBtn = document.getElementById("mic-btn")
|
| 22 |
+
const micSvg = document.getElementById("mic-svg")
|
| 23 |
+
const waveformEl = document.getElementById("waveform")
|
| 24 |
+
const signLabel = document.getElementById("sign-label")
|
| 25 |
+
const transcriptLabel = document.getElementById("transcript-label")
|
| 26 |
+
const transcriptText = document.getElementById("transcript-text")
|
| 27 |
+
const glossTokensEl = document.getElementById("gloss-tokens")
|
| 28 |
+
|
| 29 |
+
const replayBtn = document.getElementById("replay-btn")
|
| 30 |
+
const textInput = document.getElementById("text-input")
|
| 31 |
+
const sendBtn = document.getElementById("send-btn")
|
| 32 |
+
|
| 33 |
+
let lastSignSequence = []
|
| 34 |
+
|
| 35 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 36 |
+
THREE.JS SCENE
|
| 37 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 38 |
+
|
| 39 |
+
const scene = new THREE.Scene()
|
| 40 |
+
|
| 41 |
+
const camera = new THREE.PerspectiveCamera(
|
| 42 |
+
45,
|
| 43 |
+
window.innerWidth / window.innerHeight,
|
| 44 |
+
0.1,
|
| 45 |
+
1000
|
| 46 |
+
)
|
| 47 |
+
camera.position.set(0, 1.25, 2.4)
|
| 48 |
+
camera.lookAt(0, 1.25, 0)
|
| 49 |
+
|
| 50 |
+
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
|
| 51 |
+
renderer.setSize(window.innerWidth, window.innerHeight)
|
| 52 |
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
| 53 |
+
renderer.shadowMap.enabled = true
|
| 54 |
+
renderer.setClearColor(0x000000, 0) // transparent for gradient bg
|
| 55 |
+
|
| 56 |
+
document.body.prepend(renderer.domElement)
|
| 57 |
+
renderer.domElement.id = "avatar-canvas"
|
| 58 |
+
|
| 59 |
+
window.addEventListener("resize", () => {
|
| 60 |
+
camera.aspect = window.innerWidth / window.innerHeight
|
| 61 |
+
camera.updateProjectionMatrix()
|
| 62 |
+
renderer.setSize(window.innerWidth, window.innerHeight)
|
| 63 |
+
})
|
| 64 |
+
|
| 65 |
+
/* βββ Lighting βββββββββββββββββββββββββββββββββββββββββββ */
|
| 66 |
+
const keyLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
| 67 |
+
keyLight.position.set(2, 4, 3)
|
| 68 |
+
scene.add(keyLight)
|
| 69 |
+
|
| 70 |
+
const fillLight = new THREE.DirectionalLight(0x93b5ff, 0.5)
|
| 71 |
+
fillLight.position.set(-2, 2, 2)
|
| 72 |
+
scene.add(fillLight)
|
| 73 |
+
|
| 74 |
+
const rimLight = new THREE.DirectionalLight(0xc084fc, 0.4)
|
| 75 |
+
rimLight.position.set(0, 2, -3)
|
| 76 |
+
scene.add(rimLight)
|
| 77 |
+
|
| 78 |
+
const ambient = new THREE.AmbientLight(0xffffff, 0.35)
|
| 79 |
+
scene.add(ambient)
|
| 80 |
+
|
| 81 |
+
const signSpot = new THREE.SpotLight(0xffffff, 1.5)
|
| 82 |
+
signSpot.position.set(0, 3, 2)
|
| 83 |
+
signSpot.target.position.set(0, 1.4, 0)
|
| 84 |
+
scene.add(signSpot)
|
| 85 |
+
scene.add(signSpot.target)
|
| 86 |
+
|
| 87 |
+
/* βββ Avatar βββββββββββββββββββββββββββββββββββββββββββββ */
|
| 88 |
+
const loader = new GLTFLoader()
|
| 89 |
+
|
| 90 |
+
loader.load("avatar.glb", (gltf) => {
|
| 91 |
+
avatar = gltf.scene
|
| 92 |
+
scene.add(avatar)
|
| 93 |
+
|
| 94 |
+
avatar.scale.set(1.0, 1.0, 1.0)
|
| 95 |
+
avatar.position.set(0, -0.3, 0)
|
| 96 |
+
|
| 97 |
+
avatar.traverse((obj) => {
|
| 98 |
+
if (obj.isBone) {
|
| 99 |
+
bones[obj.name] = obj
|
| 100 |
+
// Save the default rest pose for applying relative offsets
|
| 101 |
+
baseRotations[obj.name] = {
|
| 102 |
+
x: obj.rotation.x,
|
| 103 |
+
y: obj.rotation.y,
|
| 104 |
+
z: obj.rotation.z
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
if (obj.isMesh) {
|
| 108 |
+
obj.frustumCulled = false // Fixes meshes (like the face) randomly getting cutoff/disappearing
|
| 109 |
+
if (obj.morphTargetDictionary) {
|
| 110 |
+
if (!window.faceMesh || Object.keys(obj.morphTargetDictionary).length > Object.keys(window.faceMesh.morphTargetDictionary).length) {
|
| 111 |
+
window.faceMesh = obj
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
})
|
| 116 |
+
|
| 117 |
+
// Start visible
|
| 118 |
+
avatar.visible = true
|
| 119 |
+
})
|
| 120 |
+
|
| 121 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 122 |
+
ANIMATION LOOP
|
| 123 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 124 |
+
|
| 125 |
+
function animate() {
|
| 126 |
+
requestAnimationFrame(animate)
|
| 127 |
+
idleTime += 0.03
|
| 128 |
+
|
| 129 |
+
// Lerp all bones toward target rotations + base resting pose
|
| 130 |
+
for (const boneName in window.targetRotations) {
|
| 131 |
+
const bone = bones[boneName]
|
| 132 |
+
const base = baseRotations[boneName]
|
| 133 |
+
if (!bone || !base) continue
|
| 134 |
+
|
| 135 |
+
const targetOffset = window.targetRotations[boneName]
|
| 136 |
+
const speed = 0.15
|
| 137 |
+
|
| 138 |
+
bone.rotation.x = THREE.MathUtils.lerp(bone.rotation.x, base.x + targetOffset.x, speed)
|
| 139 |
+
bone.rotation.y = THREE.MathUtils.lerp(bone.rotation.y, base.y + targetOffset.y, speed)
|
| 140 |
+
bone.rotation.z = THREE.MathUtils.lerp(bone.rotation.z, base.z + targetOffset.z, speed)
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// Idle breathing
|
| 144 |
+
if (!window.signing && bones.Spine) {
|
| 145 |
+
bones.Spine.rotation.x = Math.sin(idleTime) * 0.015
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
renderer.render(scene, camera)
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
animate()
|
| 152 |
+
|
| 153 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 154 |
+
HANDSHAPE SYSTEM (expanded)
|
| 155 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 156 |
+
|
| 157 |
+
const HANDSHAPES = {
|
| 158 |
+
|
| 159 |
+
OPEN: {
|
| 160 |
+
RightHandThumb1: { x: 0, y: 0, z: -0.3 },
|
| 161 |
+
RightHandThumb2: { x: 0, y: 0, z: 0 },
|
| 162 |
+
RightHandThumb3: { x: 0, y: 0, z: 0 },
|
| 163 |
+
RightHandIndex1: { x: 0, y: 0, z: 0 }, RightHandIndex2: { x: 0, y: 0, z: 0 }, RightHandIndex3: { x: 0, y: 0, z: 0 },
|
| 164 |
+
RightHandMiddle1: { x: 0, y: 0, z: 0 }, RightHandMiddle2: { x: 0, y: 0, z: 0 }, RightHandMiddle3: { x: 0, y: 0, z: 0 },
|
| 165 |
+
RightHandRing1: { x: 0, y: 0, z: 0 }, RightHandRing2: { x: 0, y: 0, z: 0 }, RightHandRing3: { x: 0, y: 0, z: 0 },
|
| 166 |
+
RightHandPinky1: { x: 0, y: 0, z: 0 }, RightHandPinky2: { x: 0, y: 0, z: 0 }, RightHandPinky3: { x: 0, y: 0, z: 0 },
|
| 167 |
+
RightHand: { x: 0, y: 0, z: 0 },
|
| 168 |
+
},
|
| 169 |
+
|
| 170 |
+
FIST: {
|
| 171 |
+
RightHandThumb1: { x: 0.3, y: 0, z: 0.3 },
|
| 172 |
+
RightHandThumb2: { x: 0.3, y: 0, z: 0 },
|
| 173 |
+
RightHandThumb3: { x: 0.2, y: 0, z: 0 },
|
| 174 |
+
RightHandIndex1: { x: 1.2, y: 0, z: 0 }, RightHandIndex2: { x: 1.2, y: 0, z: 0 }, RightHandIndex3: { x: 1.0, y: 0, z: 0 },
|
| 175 |
+
RightHandMiddle1: { x: 1.2, y: 0, z: 0 }, RightHandMiddle2: { x: 1.2, y: 0, z: 0 }, RightHandMiddle3: { x: 1.0, y: 0, z: 0 },
|
| 176 |
+
RightHandRing1: { x: 1.2, y: 0, z: 0 }, RightHandRing2: { x: 1.2, y: 0, z: 0 }, RightHandRing3: { x: 1.0, y: 0, z: 0 },
|
| 177 |
+
RightHandPinky1: { x: 1.2, y: 0, z: 0 }, RightHandPinky2: { x: 1.2, y: 0, z: 0 }, RightHandPinky3: { x: 1.0, y: 0, z: 0 },
|
| 178 |
+
RightHand: { x: 0, y: 0, z: 0 },
|
| 179 |
+
},
|
| 180 |
+
|
| 181 |
+
POINT: {
|
| 182 |
+
RightHandThumb1: { x: 0.5, y: 0, z: 0.3 },
|
| 183 |
+
RightHandThumb2: { x: 0.4, y: 0, z: 0 },
|
| 184 |
+
RightHandThumb3: { x: 0.2, y: 0, z: 0 },
|
| 185 |
+
RightHandIndex1: { x: 0, y: 0, z: 0 }, RightHandIndex2: { x: 0, y: 0, z: 0 }, RightHandIndex3: { x: 0, y: 0, z: 0 },
|
| 186 |
+
RightHandMiddle1: { x: 1.2, y: 0, z: 0 }, RightHandMiddle2: { x: 1.0, y: 0, z: 0 }, RightHandMiddle3: { x: 0.8, y: 0, z: 0 },
|
| 187 |
+
RightHandRing1: { x: 1.2, y: 0, z: 0 }, RightHandRing2: { x: 1.0, y: 0, z: 0 }, RightHandRing3: { x: 0.8, y: 0, z: 0 },
|
| 188 |
+
RightHandPinky1: { x: 1.2, y: 0, z: 0 }, RightHandPinky2: { x: 1.0, y: 0, z: 0 }, RightHandPinky3: { x: 0.8, y: 0, z: 0 },
|
| 189 |
+
RightHand: { x: 0, y: 0, z: 0 },
|
| 190 |
+
},
|
| 191 |
+
|
| 192 |
+
FLAT: {
|
| 193 |
+
RightHandThumb1: { x: 0.2, y: 0, z: 0.3 },
|
| 194 |
+
RightHandThumb2: { x: 0.1, y: 0, z: 0 },
|
| 195 |
+
RightHandThumb3: { x: 0, y: 0, z: 0 },
|
| 196 |
+
RightHandIndex1: { x: 0, y: 0, z: 0 }, RightHandIndex2: { x: 0, y: 0, z: 0 }, RightHandIndex3: { x: 0, y: 0, z: 0 },
|
| 197 |
+
RightHandMiddle1: { x: 0, y: 0, z: 0 }, RightHandMiddle2: { x: 0, y: 0, z: 0 }, RightHandMiddle3: { x: 0, y: 0, z: 0 },
|
| 198 |
+
RightHandRing1: { x: 0, y: 0, z: 0 }, RightHandRing2: { x: 0, y: 0, z: 0 }, RightHandRing3: { x: 0, y: 0, z: 0 },
|
| 199 |
+
RightHandPinky1: { x: 0, y: 0, z: 0 }, RightHandPinky2: { x: 0, y: 0, z: 0 }, RightHandPinky3: { x: 0, y: 0, z: 0 },
|
| 200 |
+
RightHand: { x: 0, y: 0, z: 0 },
|
| 201 |
+
},
|
| 202 |
+
|
| 203 |
+
CLAW: {
|
| 204 |
+
RightHandThumb1: { x: 0.4, y: 0, z: -0.2 },
|
| 205 |
+
RightHandThumb2: { x: 0.3, y: 0, z: 0 },
|
| 206 |
+
RightHandThumb3: { x: 0.2, y: 0, z: 0 },
|
| 207 |
+
RightHandIndex1: { x: 0.6, y: 0, z: 0 }, RightHandIndex2: { x: 0.5, y: 0, z: 0 }, RightHandIndex3: { x: 0.4, y: 0, z: 0 },
|
| 208 |
+
RightHandMiddle1: { x: 0.6, y: 0, z: 0 }, RightHandMiddle2: { x: 0.5, y: 0, z: 0 }, RightHandMiddle3: { x: 0.4, y: 0, z: 0 },
|
| 209 |
+
RightHandRing1: { x: 0.6, y: 0, z: 0 }, RightHandRing2: { x: 0.5, y: 0, z: 0 }, RightHandRing3: { x: 0.4, y: 0, z: 0 },
|
| 210 |
+
RightHandPinky1: { x: 0.6, y: 0, z: 0 }, RightHandPinky2: { x: 0.5, y: 0, z: 0 }, RightHandPinky3: { x: 0.4, y: 0, z: 0 },
|
| 211 |
+
RightHand: { x: 0, y: 0, z: 0 },
|
| 212 |
+
},
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
function applyHandshape(shape) {
|
| 216 |
+
if (!bones.RightHandIndex1) return
|
| 217 |
+
|
| 218 |
+
const shapeUpper = (shape || "OPEN").toUpperCase()
|
| 219 |
+
|
| 220 |
+
let pose = HANDSHAPES.OPEN // Default
|
| 221 |
+
|
| 222 |
+
// Check built-in shapes first
|
| 223 |
+
if (HANDSHAPES[shapeUpper]) {
|
| 224 |
+
pose = HANDSHAPES[shapeUpper]
|
| 225 |
+
} else if (FINGERSPELL[shapeUpper]) {
|
| 226 |
+
// Check the fingerspell dictionary for letter-based handshapes
|
| 227 |
+
pose = {}
|
| 228 |
+
for (const key in FINGERSPELL[shapeUpper]) {
|
| 229 |
+
const boneName = FINGER_BONE_MAP[key]
|
| 230 |
+
if (boneName) pose[boneName] = FINGERSPELL[shapeUpper][key]
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
for (const bone in pose) {
|
| 235 |
+
window.targetRotations[bone] = { ...pose[bone] }
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 240 |
+
LOCATION SYSTEM (expanded)
|
| 241 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 242 |
+
|
| 243 |
+
const LOCATIONS = {
|
| 244 |
+
neutral_space: {
|
| 245 |
+
RightShoulder: { x: 0, y: 0, z: -0.15 },
|
| 246 |
+
RightArm: { x: -0.3, y: 0, z: 0 },
|
| 247 |
+
RightForeArm: { x: -0.4, y: 0.2, z: 0 },
|
| 248 |
+
},
|
| 249 |
+
chest: {
|
| 250 |
+
RightShoulder: { x: 0, y: 0, z: -0.2 },
|
| 251 |
+
RightArm: { x: -0.5, y: 0, z: 0 },
|
| 252 |
+
RightForeArm: { x: -0.6, y: 0.3, z: 0 },
|
| 253 |
+
},
|
| 254 |
+
chin: {
|
| 255 |
+
RightShoulder: { x: 0, y: 0, z: -0.3 },
|
| 256 |
+
RightArm: { x: -0.7, y: 0.1, z: 0 },
|
| 257 |
+
RightForeArm: { x: -0.85, y: 0.3, z: 0 },
|
| 258 |
+
},
|
| 259 |
+
mouth: {
|
| 260 |
+
RightShoulder: { x: 0, y: 0, z: -0.3 },
|
| 261 |
+
RightArm: { x: -0.75, y: 0.1, z: 0 },
|
| 262 |
+
RightForeArm: { x: -0.9, y: 0.3, z: 0 },
|
| 263 |
+
},
|
| 264 |
+
nose: {
|
| 265 |
+
RightShoulder: { x: 0, y: 0, z: -0.32 },
|
| 266 |
+
RightArm: { x: -0.85, y: 0.1, z: 0 },
|
| 267 |
+
RightForeArm: { x: -1.0, y: 0.3, z: 0 },
|
| 268 |
+
},
|
| 269 |
+
forehead: {
|
| 270 |
+
RightShoulder: { x: 0, y: 0, z: -0.35 },
|
| 271 |
+
RightArm: { x: -0.9, y: 0.1, z: 0 },
|
| 272 |
+
RightForeArm: { x: -1.05, y: 0.3, z: 0 },
|
| 273 |
+
},
|
| 274 |
+
temple: {
|
| 275 |
+
RightShoulder: { x: 0, y: 0, z: -0.35 },
|
| 276 |
+
RightArm: { x: -0.85, y: 0.15, z: 0.1 },
|
| 277 |
+
RightForeArm: { x: -1.0, y: 0.35, z: 0 },
|
| 278 |
+
},
|
| 279 |
+
side: {
|
| 280 |
+
RightShoulder: { x: 0, y: 0, z: -0.1 },
|
| 281 |
+
RightArm: { x: -0.3, y: 0, z: 0.3 },
|
| 282 |
+
RightForeArm: { x: -0.4, y: 0.2, z: 0 },
|
| 283 |
+
},
|
| 284 |
+
shoulder: {
|
| 285 |
+
RightShoulder: { x: 0, y: 0, z: -0.15 },
|
| 286 |
+
RightArm: { x: -0.4, y: 0, z: 0.15 },
|
| 287 |
+
RightForeArm: { x: -0.7, y: 0.2, z: 0 },
|
| 288 |
+
},
|
| 289 |
+
ear: {
|
| 290 |
+
RightShoulder: { x: 0, y: 0, z: -0.35 },
|
| 291 |
+
RightArm: { x: -0.85, y: 0.2, z: 0.2 },
|
| 292 |
+
RightForeArm: { x: -1.0, y: 0.35, z: 0 },
|
| 293 |
+
},
|
| 294 |
+
waist: {
|
| 295 |
+
RightShoulder: { x: 0, y: 0, z: -0.1 },
|
| 296 |
+
RightArm: { x: -0.2, y: 0, z: 0 },
|
| 297 |
+
RightForeArm: { x: -0.3, y: 0.15, z: 0 },
|
| 298 |
+
},
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
function applyLocation(loc) {
|
| 302 |
+
if (!bones.RightArm) return
|
| 303 |
+
|
| 304 |
+
const key = (loc || "neutral_space").toLowerCase()
|
| 305 |
+
const pose = LOCATIONS[key] || LOCATIONS.neutral_space
|
| 306 |
+
|
| 307 |
+
for (const bone in pose) {
|
| 308 |
+
if (window.targetRotations) {
|
| 309 |
+
window.targetRotations[bone] = { ...pose[bone] }
|
| 310 |
+
}
|
| 311 |
+
}
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 315 |
+
FINGERSPELLING (from dictionary)
|
| 316 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 317 |
+
|
| 318 |
+
function applyFingerSpell(letter) {
|
| 319 |
+
const l = (letter || "").toUpperCase()
|
| 320 |
+
const pose = FINGERSPELL[l]
|
| 321 |
+
if (!pose) return
|
| 322 |
+
|
| 323 |
+
for (const key in pose) {
|
| 324 |
+
const boneName = FINGER_BONE_MAP[key]
|
| 325 |
+
if (boneName) {
|
| 326 |
+
window.targetRotations[boneName] = { ...pose[key] }
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
applyLocation("neutral_space")
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
/* βββ Expose to signEngine βββββββββββββββββββββββββββββββ */
|
| 334 |
+
window.applyHandshape = applyHandshape
|
| 335 |
+
window.applyLocation = applyLocation
|
| 336 |
+
window.applyFingerSpell = applyFingerSpell
|
| 337 |
+
window.playSentence = playSentence
|
| 338 |
+
|
| 339 |
+
window.clearPose = function() {
|
| 340 |
+
for (const boneName in window.targetRotations) {
|
| 341 |
+
if (window.targetRotations[boneName]) {
|
| 342 |
+
window.targetRotations[boneName] = { x: 0, y: 0, z: 0 }
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 348 |
+
FACIAL EXPRESSIONS (Morph Targets)
|
| 349 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 350 |
+
const EXPRESSIONS = {
|
| 351 |
+
neutral: {},
|
| 352 |
+
happy: { mouthSmile: 1.0 },
|
| 353 |
+
sad: { mouthOpen: 0.1 },
|
| 354 |
+
surprise: { mouthOpen: 0.8, mouthSmile: 0.2 },
|
| 355 |
+
angry: { mouthOpen: 0.3 },
|
| 356 |
+
question: { mouthOpen: 0.2 }
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
window.applyExpression = function(exprName) {
|
| 360 |
+
if (!window.faceMesh || !window.faceMesh.morphTargetDictionary) return
|
| 361 |
+
|
| 362 |
+
const dict = window.faceMesh.morphTargetDictionary
|
| 363 |
+
const influences = window.faceMesh.morphTargetInfluences
|
| 364 |
+
|
| 365 |
+
// Reset all
|
| 366 |
+
for (const key in dict) {
|
| 367 |
+
influences[dict[key]] = 0
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
const expr = EXPRESSIONS[(exprName || "neutral").toLowerCase()]
|
| 371 |
+
if (!expr) return
|
| 372 |
+
|
| 373 |
+
for (const key in expr) {
|
| 374 |
+
let targetIdx = dict[key]
|
| 375 |
+
if (targetIdx === undefined) {
|
| 376 |
+
// Find case-insensitive partial match
|
| 377 |
+
const matchingKey = Object.keys(dict).find(k => k.toLowerCase().includes(key.toLowerCase()))
|
| 378 |
+
if (matchingKey) targetIdx = dict[matchingKey]
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
if (targetIdx !== undefined) {
|
| 382 |
+
influences[targetIdx] = expr[key]
|
| 383 |
+
}
|
| 384 |
+
}
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 388 |
+
SIGN ENGINE CALLBACKS (UI updates)
|
| 389 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 390 |
+
|
| 391 |
+
setCallbacks({
|
| 392 |
+
onSignStart: (index, sign) => {
|
| 393 |
+
// Highlight active gloss token
|
| 394 |
+
const tokens = glossTokensEl.querySelectorAll(".gloss-token")
|
| 395 |
+
tokens.forEach((el, i) => {
|
| 396 |
+
el.classList.remove("active")
|
| 397 |
+
if (i < index) el.classList.add("done")
|
| 398 |
+
if (i === index) el.classList.add("active")
|
| 399 |
+
})
|
| 400 |
+
|
| 401 |
+
const label = sign.type === "fingerspell"
|
| 402 |
+
? `Spelling: ${sign.letter || sign.handshape}`
|
| 403 |
+
: `Signing: ${sign.gloss || sign.handshape}`
|
| 404 |
+
|
| 405 |
+
signLabel.textContent = label
|
| 406 |
+
signLabel.classList.add("visible")
|
| 407 |
+
},
|
| 408 |
+
|
| 409 |
+
onSignEnd: () => {
|
| 410 |
+
signLabel.classList.remove("visible")
|
| 411 |
+
const tokens = glossTokensEl.querySelectorAll(".gloss-token")
|
| 412 |
+
tokens.forEach(el => el.classList.add("done"))
|
| 413 |
+
|
| 414 |
+
setUIState("ready")
|
| 415 |
+
},
|
| 416 |
+
|
| 417 |
+
onLetterStart: (letter) => {
|
| 418 |
+
signLabel.textContent = `Spelling: ${letter}`
|
| 419 |
+
}
|
| 420 |
+
})
|
| 421 |
+
|
| 422 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 423 |
+
VOICE RECORDING β VAD (Voice Activity Detection)
|
| 424 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 425 |
+
|
| 426 |
+
let recorder = null
|
| 427 |
+
let audioChunks = []
|
| 428 |
+
let audioStream = null
|
| 429 |
+
let audioContext = null
|
| 430 |
+
let analyser = null
|
| 431 |
+
let silenceTimer = null
|
| 432 |
+
let isRecording = false
|
| 433 |
+
|
| 434 |
+
const SILENCE_THRESHOLD = 0.015 // RMS threshold for "silence"
|
| 435 |
+
const SILENCE_DURATION = 10000 // ms of silence before auto-send (updated to 10 seconds)
|
| 436 |
+
const MIN_RECORD_TIME = 500 // ms minimum recording
|
| 437 |
+
|
| 438 |
+
// Create waveform bars
|
| 439 |
+
for (let i = 0; i < 24; i++) {
|
| 440 |
+
const bar = document.createElement("div")
|
| 441 |
+
bar.className = "wave-bar"
|
| 442 |
+
waveformEl.appendChild(bar)
|
| 443 |
+
}
|
| 444 |
+
const waveBars = waveformEl.querySelectorAll(".wave-bar")
|
| 445 |
+
|
| 446 |
+
async function startRecording() {
|
| 447 |
+
if (isRecording) {
|
| 448 |
+
// Toggle off β stop recording and send
|
| 449 |
+
stopAndSend()
|
| 450 |
+
return
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
try {
|
| 454 |
+
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
| 455 |
+
} catch (err) {
|
| 456 |
+
console.error("Mic access denied:", err)
|
| 457 |
+
setUIState("ready")
|
| 458 |
+
return
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
isRecording = true
|
| 462 |
+
audioChunks = []
|
| 463 |
+
|
| 464 |
+
// Set up MediaRecorder
|
| 465 |
+
recorder = new MediaRecorder(audioStream)
|
| 466 |
+
recorder.ondataavailable = e => audioChunks.push(e.data)
|
| 467 |
+
recorder.onstop = sendAudioToBackend
|
| 468 |
+
recorder.start()
|
| 469 |
+
|
| 470 |
+
// Set up audio analysis for VAD + waveform
|
| 471 |
+
audioContext = new AudioContext()
|
| 472 |
+
const source = audioContext.createMediaStreamSource(audioStream)
|
| 473 |
+
analyser = audioContext.createAnalyser()
|
| 474 |
+
analyser.fftSize = 256
|
| 475 |
+
source.connect(analyser)
|
| 476 |
+
|
| 477 |
+
setUIState("recording")
|
| 478 |
+
monitorAudio()
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
function monitorAudio() {
|
| 482 |
+
if (!isRecording || !analyser) return
|
| 483 |
+
|
| 484 |
+
const data = new Uint8Array(analyser.frequencyBinCount)
|
| 485 |
+
analyser.getByteTimeDomainData(data)
|
| 486 |
+
|
| 487 |
+
// Calculate RMS
|
| 488 |
+
let sum = 0
|
| 489 |
+
for (let i = 0; i < data.length; i++) {
|
| 490 |
+
const normalized = (data[i] - 128) / 128
|
| 491 |
+
sum += normalized * normalized
|
| 492 |
+
}
|
| 493 |
+
const rms = Math.sqrt(sum / data.length)
|
| 494 |
+
|
| 495 |
+
// Update waveform visual
|
| 496 |
+
updateWaveform(data)
|
| 497 |
+
|
| 498 |
+
// Voice activity detection
|
| 499 |
+
if (rms > SILENCE_THRESHOLD) {
|
| 500 |
+
// Voice detected β reset silence timer
|
| 501 |
+
if (silenceTimer) {
|
| 502 |
+
clearTimeout(silenceTimer)
|
| 503 |
+
silenceTimer = null
|
| 504 |
+
}
|
| 505 |
+
} else {
|
| 506 |
+
// Silence β start countdown if not already
|
| 507 |
+
if (!silenceTimer && audioChunks.length > 0) {
|
| 508 |
+
silenceTimer = setTimeout(() => {
|
| 509 |
+
if (isRecording) stopAndSend()
|
| 510 |
+
}, SILENCE_DURATION)
|
| 511 |
+
}
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
requestAnimationFrame(monitorAudio)
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
function updateWaveform(data) {
|
| 518 |
+
const step = Math.floor(data.length / waveBars.length)
|
| 519 |
+
waveBars.forEach((bar, i) => {
|
| 520 |
+
const value = Math.abs(data[i * step] - 128) / 128
|
| 521 |
+
const height = Math.max(4, value * 36)
|
| 522 |
+
bar.style.height = `${height}px`
|
| 523 |
+
})
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
function stopAndSend() {
|
| 527 |
+
isRecording = false
|
| 528 |
+
if (silenceTimer) { clearTimeout(silenceTimer); silenceTimer = null }
|
| 529 |
+
|
| 530 |
+
if (recorder && recorder.state === "recording") {
|
| 531 |
+
recorder.stop()
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
if (audioStream) {
|
| 535 |
+
audioStream.getTracks().forEach(t => t.stop())
|
| 536 |
+
audioStream = null
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
if (audioContext) {
|
| 540 |
+
audioContext.close()
|
| 541 |
+
audioContext = null
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
setUIState("processing")
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 548 |
+
SEND TO BACKEND
|
| 549 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 550 |
+
|
| 551 |
+
async function sendAudioToBackend() {
|
| 552 |
+
const blob = new Blob(audioChunks, { type: "audio/webm" })
|
| 553 |
+
const formData = new FormData()
|
| 554 |
+
formData.append("file", blob, "recording.webm")
|
| 555 |
+
|
| 556 |
+
try {
|
| 557 |
+
const response = await fetch("/voice-to-text/", {
|
| 558 |
+
method: "POST",
|
| 559 |
+
body: formData
|
| 560 |
+
})
|
| 561 |
+
|
| 562 |
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
| 563 |
+
|
| 564 |
+
const data = await response.json()
|
| 565 |
+
console.log("Backend response:", data)
|
| 566 |
+
|
| 567 |
+
displayTranscript(data)
|
| 568 |
+
|
| 569 |
+
if (data.sign_sequence && data.sign_sequence.length > 0) {
|
| 570 |
+
lastSignSequence = data.sign_sequence
|
| 571 |
+
replayBtn.disabled = false
|
| 572 |
+
setUIState("signing")
|
| 573 |
+
if (avatar) avatar.visible = true
|
| 574 |
+
playSentence(data.sign_sequence)
|
| 575 |
+
} else {
|
| 576 |
+
setUIState("ready")
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
} catch (err) {
|
| 580 |
+
console.error("Backend error:", err)
|
| 581 |
+
transcriptLabel.textContent = ""
|
| 582 |
+
transcriptText.textContent = "Connection error β is the backend running?"
|
| 583 |
+
transcriptText.classList.add("visible")
|
| 584 |
+
setUIState("ready")
|
| 585 |
+
}
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 589 |
+
UI STATE MANAGEMENT
|
| 590 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 591 |
+
|
| 592 |
+
function setUIState(state) {
|
| 593 |
+
micBtn.className = ""
|
| 594 |
+
statusDot.className = "status-dot"
|
| 595 |
+
waveformEl.classList.remove("active")
|
| 596 |
+
|
| 597 |
+
switch (state) {
|
| 598 |
+
case "ready":
|
| 599 |
+
statusText.textContent = "Ready"
|
| 600 |
+
statusDot.className = "status-dot"
|
| 601 |
+
micSvg.innerHTML = '<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5z"/><path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>'
|
| 602 |
+
break
|
| 603 |
+
|
| 604 |
+
case "recording":
|
| 605 |
+
statusText.textContent = "Listening..."
|
| 606 |
+
statusDot.className = "status-dot recording"
|
| 607 |
+
micBtn.className = "recording"
|
| 608 |
+
waveformEl.classList.add("active")
|
| 609 |
+
// Stop icon
|
| 610 |
+
micSvg.innerHTML = '<rect x="7" y="7" width="10" height="10" rx="1" fill="white"/>'
|
| 611 |
+
break
|
| 612 |
+
|
| 613 |
+
case "processing":
|
| 614 |
+
statusText.textContent = "Processing..."
|
| 615 |
+
statusDot.className = "status-dot processing"
|
| 616 |
+
micBtn.className = "processing"
|
| 617 |
+
// Spinner icon
|
| 618 |
+
micSvg.innerHTML = '<circle cx="12" cy="12" r="8" stroke="white" stroke-width="2" fill="none" stroke-dasharray="20 30"/>'
|
| 619 |
+
break
|
| 620 |
+
|
| 621 |
+
case "signing":
|
| 622 |
+
statusText.textContent = "Signing..."
|
| 623 |
+
statusDot.className = "status-dot signing"
|
| 624 |
+
break
|
| 625 |
+
}
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
function displayTranscript(data) {
|
| 629 |
+
// Show original transcription
|
| 630 |
+
transcriptLabel.textContent = "TRANSCRIPTION"
|
| 631 |
+
transcriptText.textContent = data.cleaned_transcription || data.raw_transcription || ""
|
| 632 |
+
transcriptText.classList.add("visible")
|
| 633 |
+
|
| 634 |
+
// Show gloss tokens
|
| 635 |
+
glossTokensEl.innerHTML = ""
|
| 636 |
+
if (data.sign_friendly_text && data.sign_friendly_text.length > 0) {
|
| 637 |
+
data.sign_friendly_text.forEach(word => {
|
| 638 |
+
const el = document.createElement("span")
|
| 639 |
+
el.className = "gloss-token"
|
| 640 |
+
el.textContent = word
|
| 641 |
+
glossTokensEl.appendChild(el)
|
| 642 |
+
})
|
| 643 |
+
}
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
/* βββ Mic button handler βββββββββββββββββββββββββββββββββ */
|
| 647 |
+
micBtn.onclick = startRecording
|
| 648 |
+
|
| 649 |
+
/* βββ Text Input handlers ββββββββββββββββββββββββββββββββ */
|
| 650 |
+
async function sendTextToBackend() {
|
| 651 |
+
const text = textInput.value.trim()
|
| 652 |
+
if (!text) return
|
| 653 |
+
|
| 654 |
+
textInput.value = ""
|
| 655 |
+
setUIState("processing")
|
| 656 |
+
|
| 657 |
+
try {
|
| 658 |
+
const response = await fetch("/text-to-sign/", {
|
| 659 |
+
method: "POST",
|
| 660 |
+
headers: { "Content-Type": "application/json" },
|
| 661 |
+
body: JSON.stringify({ text })
|
| 662 |
+
})
|
| 663 |
+
|
| 664 |
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
| 665 |
+
|
| 666 |
+
const data = await response.json()
|
| 667 |
+
console.log("Text backend response:", data)
|
| 668 |
+
|
| 669 |
+
displayTranscript(data)
|
| 670 |
+
|
| 671 |
+
if (data.sign_sequence && data.sign_sequence.length > 0) {
|
| 672 |
+
lastSignSequence = data.sign_sequence
|
| 673 |
+
replayBtn.disabled = false
|
| 674 |
+
setUIState("signing")
|
| 675 |
+
if (avatar) avatar.visible = true
|
| 676 |
+
playSentence(data.sign_sequence)
|
| 677 |
+
} else {
|
| 678 |
+
setUIState("ready")
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
} catch (err) {
|
| 682 |
+
console.error("Text backend error:", err)
|
| 683 |
+
transcriptLabel.textContent = ""
|
| 684 |
+
transcriptText.textContent = "Connection error β is the backend running?"
|
| 685 |
+
transcriptText.classList.add("visible")
|
| 686 |
+
setUIState("ready")
|
| 687 |
+
}
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
sendBtn.onclick = sendTextToBackend
|
| 691 |
+
textInput.onkeydown = (e) => {
|
| 692 |
+
if (e.key === "Enter") sendTextToBackend()
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
/* βββ Replay handler ββββββββββββββββββββββββββββββββββββ */
|
| 696 |
+
replayBtn.onclick = () => {
|
| 697 |
+
if (lastSignSequence.length > 0) {
|
| 698 |
+
// Re-trigger the tokens visualization
|
| 699 |
+
displayTranscript({
|
| 700 |
+
cleaned_transcription: transcriptText.textContent,
|
| 701 |
+
sign_friendly_text: Array.from(glossTokensEl.querySelectorAll(".gloss-token")).map(el => el.textContent),
|
| 702 |
+
sign_sequence: lastSignSequence
|
| 703 |
+
})
|
| 704 |
+
|
| 705 |
+
setUIState("signing")
|
| 706 |
+
playSentence(lastSignSequence)
|
| 707 |
+
}
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
/* βββ Initial state ββββββββββββββββββββββββββββββββββββββ */
|
| 711 |
+
setUIState("ready")
|
| 712 |
+
/* βββ Theme Toggle βββββββββββββββββββββββββββββββββββββββ */
|
| 713 |
+
const themeBtn = document.getElementById("theme-btn")
|
| 714 |
+
if (themeBtn) {
|
| 715 |
+
themeBtn.onclick = () => {
|
| 716 |
+
document.body.classList.toggle("light-mode")
|
| 717 |
+
const isLight = document.body.classList.contains("light-mode")
|
| 718 |
+
localStorage.setItem("signapp-theme", isLight ? "light" : "dark")
|
| 719 |
+
updateThemeIcon(isLight)
|
| 720 |
+
|
| 721 |
+
// Also update scene lights to match a bit better in light mode
|
| 722 |
+
if (isLight) {
|
| 723 |
+
scene.background = new THREE.Color(0xf1f5f9)
|
| 724 |
+
} else {
|
| 725 |
+
scene.background = null // transparent
|
| 726 |
+
}
|
| 727 |
+
}
|
| 728 |
+
}
|
| 729 |
+
function updateThemeIcon(isLight) {
|
| 730 |
+
const icon = document.getElementById("theme-icon")
|
| 731 |
+
if (!icon) return
|
| 732 |
+
if (isLight) {
|
| 733 |
+
icon.innerHTML = '<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-3.03 0-5.5-2.47-5.5-5.5 0-1.82.89-3.42 2.26-4.4C12.92 3.04 12.46 3 12 3zm0 16c-3.86 0-7-3.14-7-7s3.14-7 7-7c.22 0 .44.03.65.08C11.13 6.36 10 7.97 10 9.85c0 2.85 2.3 5.15 5.15 5.15 1.88 0 3.49-1.13 4.77-2.65.05.21.08.43.08.65 0 3.86-3.14 7-7 7z"/>'
|
| 734 |
+
} else {
|
| 735 |
+
icon.innerHTML = '<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41.39.39 1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41.39.39 1.03.39 1.41 0l1.06-1.06z"/>'
|
| 736 |
+
}
|
| 737 |
+
}
|
| 738 |
+
if (localStorage.getItem("signapp-theme") === "light") {
|
| 739 |
+
document.body.classList.add("light-mode")
|
| 740 |
+
updateThemeIcon(true)
|
| 741 |
+
scene.background = new THREE.Color(0xf1f5f9)
|
| 742 |
+
}
|
src/sign_app/ui/signEngine.js
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FINGERSPELL, FINGER_BONE_MAP } from "./fingerspellDictionary.js"
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Sign Engine β orchestrates sign playback, movements, and fingerspelling
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
/* ββ State βββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 8 |
+
let _currentSignIndex = -1
|
| 9 |
+
let _playing = false
|
| 10 |
+
|
| 11 |
+
/* ββ Exported callbacks (set by main.js) βββββββββββββββββ */
|
| 12 |
+
export let onSignStart = null // (index, sign) => void
|
| 13 |
+
export let onSignEnd = null // () => void
|
| 14 |
+
export let onLetterStart = null // (letter) => void
|
| 15 |
+
|
| 16 |
+
export function setCallbacks({ onSignStart: s, onSignEnd: e, onLetterStart: l }) {
|
| 17 |
+
onSignStart = s
|
| 18 |
+
onSignEnd = e
|
| 19 |
+
onLetterStart = l
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/* ββ Play a full sentence of signs βββββββββββββββββββββββ */
|
| 23 |
+
export function playSentence(signs) {
|
| 24 |
+
if (_playing) return
|
| 25 |
+
_playing = true
|
| 26 |
+
_currentSignIndex = 0
|
| 27 |
+
|
| 28 |
+
window.signing = true
|
| 29 |
+
|
| 30 |
+
playNext(signs)
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function playNext(signs) {
|
| 34 |
+
if (_currentSignIndex >= signs.length) {
|
| 35 |
+
// Done β return to neutral after a brief hold
|
| 36 |
+
setTimeout(() => {
|
| 37 |
+
returnToNeutral()
|
| 38 |
+
_playing = false
|
| 39 |
+
_currentSignIndex = -1
|
| 40 |
+
window.signing = false
|
| 41 |
+
if (onSignEnd) onSignEnd()
|
| 42 |
+
}, 800)
|
| 43 |
+
return
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
const sign = signs[_currentSignIndex]
|
| 47 |
+
|
| 48 |
+
if (onSignStart) onSignStart(_currentSignIndex, sign)
|
| 49 |
+
|
| 50 |
+
if (sign.type === "fingerspell") {
|
| 51 |
+
playFingerspellLetter(sign, () => {
|
| 52 |
+
_currentSignIndex++
|
| 53 |
+
setTimeout(() => playNext(signs), 250) // short gap between letters
|
| 54 |
+
})
|
| 55 |
+
} else {
|
| 56 |
+
playSign(sign, () => {
|
| 57 |
+
_currentSignIndex++
|
| 58 |
+
setTimeout(() => playNext(signs), 700) // longer gap between signs
|
| 59 |
+
})
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* ββ Play a single sign (handshape + location + movement + expression) ββ */
|
| 64 |
+
function playSign(sign, done) {
|
| 65 |
+
// 1. Apply handshape
|
| 66 |
+
applyHandshapePose(sign.handshape)
|
| 67 |
+
|
| 68 |
+
// 2. Apply location (arm positioning)
|
| 69 |
+
window.applyLocation(sign.location)
|
| 70 |
+
|
| 71 |
+
// 3. Apply expression
|
| 72 |
+
if (window.applyExpression) {
|
| 73 |
+
window.applyExpression(sign.expression || "neutral")
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// 4. Apply movement animation
|
| 77 |
+
const moveDuration = applyMovement(sign.movement)
|
| 78 |
+
|
| 79 |
+
// Wait for the pose to settle + movement to complete
|
| 80 |
+
setTimeout(() => {
|
| 81 |
+
if (window.applyExpression) window.applyExpression("neutral")
|
| 82 |
+
done()
|
| 83 |
+
}, Math.max(600, moveDuration + 200))
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/* ββ Fingerspell a single letter βββββββββββββββββββββββββ */
|
| 87 |
+
function playFingerspellLetter(sign, done) {
|
| 88 |
+
const letter = (sign.letter || sign.handshape || "").toUpperCase()
|
| 89 |
+
|
| 90 |
+
if (onLetterStart) onLetterStart(letter)
|
| 91 |
+
|
| 92 |
+
const pose = FINGERSPELL[letter]
|
| 93 |
+
if (!pose) {
|
| 94 |
+
done()
|
| 95 |
+
return
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// Apply full multi-joint fingerspell pose
|
| 99 |
+
for (const key in pose) {
|
| 100 |
+
const boneName = FINGER_BONE_MAP[key]
|
| 101 |
+
if (boneName && window.targetRotations !== undefined) {
|
| 102 |
+
window.targetRotations[boneName] = { ...pose[key] }
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Ensure neutral hand position for fingerspelling
|
| 107 |
+
window.applyLocation("neutral_space")
|
| 108 |
+
|
| 109 |
+
setTimeout(done, 450) // hold each letter
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/* ββ Handshape pose application ββββββββββββββββββββββββββ */
|
| 113 |
+
function applyHandshapePose(shape) {
|
| 114 |
+
if (!shape) return
|
| 115 |
+
|
| 116 |
+
// Check if this shape exists in FINGERSPELL dictionary first (reuse finger poses)
|
| 117 |
+
const shapeUpper = shape.toUpperCase()
|
| 118 |
+
if (FINGERSPELL[shapeUpper]) {
|
| 119 |
+
const pose = FINGERSPELL[shapeUpper]
|
| 120 |
+
for (const key in pose) {
|
| 121 |
+
const boneName = FINGER_BONE_MAP[key]
|
| 122 |
+
if (boneName && window.targetRotations !== undefined) {
|
| 123 |
+
window.targetRotations[boneName] = { ...pose[key] }
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
return
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Built-in handshape aliases
|
| 130 |
+
if (window.applyHandshape) {
|
| 131 |
+
window.applyHandshape(shapeUpper)
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* ββ Movement animations βββββββββββββββββββββββββββββββββ */
|
| 136 |
+
|
| 137 |
+
/**
|
| 138 |
+
* Apply a movement animation. Returns duration in ms.
|
| 139 |
+
*/
|
| 140 |
+
function applyMovement(movement) {
|
| 141 |
+
if (!movement || movement === "none") return 0
|
| 142 |
+
|
| 143 |
+
switch (movement.toLowerCase()) {
|
| 144 |
+
|
| 145 |
+
case "tap": return animateTap()
|
| 146 |
+
|
| 147 |
+
case "double_tap": return animateDoubleTap()
|
| 148 |
+
|
| 149 |
+
case "circle_clockwise": return animateCircle(1)
|
| 150 |
+
|
| 151 |
+
case "circle_counterclockwise": return animateCircle(-1)
|
| 152 |
+
|
| 153 |
+
case "forward": return animateForward()
|
| 154 |
+
|
| 155 |
+
case "pull_in": return animatePullIn()
|
| 156 |
+
|
| 157 |
+
case "down": return animateDown()
|
| 158 |
+
|
| 159 |
+
case "up": return animateUp()
|
| 160 |
+
|
| 161 |
+
case "side_to_side": return animateSideToSide()
|
| 162 |
+
|
| 163 |
+
case "nod": return animateNod()
|
| 164 |
+
|
| 165 |
+
case "twist": return animateTwist()
|
| 166 |
+
|
| 167 |
+
case "wave": return animateWave()
|
| 168 |
+
|
| 169 |
+
case "touch": return animateTouch()
|
| 170 |
+
|
| 171 |
+
default:
|
| 172 |
+
console.log("Unknown movement:", movement)
|
| 173 |
+
return 0
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/* ββ Movement implementations ββββββοΏ½οΏ½οΏ½βββββββββββββββββββββ */
|
| 178 |
+
|
| 179 |
+
function animateTap() {
|
| 180 |
+
const boneR = "RightForeArm"
|
| 181 |
+
const boneL = "LeftForeArm"
|
| 182 |
+
const currentR = getCurrentRotation(boneR)
|
| 183 |
+
const currentL = getCurrentRotation(boneL)
|
| 184 |
+
|
| 185 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x - 0.15 }
|
| 186 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x - 0.15 }
|
| 187 |
+
|
| 188 |
+
setTimeout(() => {
|
| 189 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
|
| 190 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
|
| 191 |
+
}, 200)
|
| 192 |
+
|
| 193 |
+
return 400
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
function animateDoubleTap() {
|
| 197 |
+
const boneR = "RightForeArm"
|
| 198 |
+
const boneL = "LeftForeArm"
|
| 199 |
+
const currentR = getCurrentRotation(boneR)
|
| 200 |
+
const currentL = getCurrentRotation(boneL)
|
| 201 |
+
|
| 202 |
+
const applyTap = () => {
|
| 203 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x - 0.15 }
|
| 204 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x - 0.15 }
|
| 205 |
+
}
|
| 206 |
+
const applyReset = () => {
|
| 207 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
|
| 208 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
applyTap()
|
| 212 |
+
setTimeout(() => {
|
| 213 |
+
applyReset()
|
| 214 |
+
setTimeout(() => {
|
| 215 |
+
applyTap()
|
| 216 |
+
setTimeout(() => applyReset(), 150)
|
| 217 |
+
}, 200)
|
| 218 |
+
}, 150)
|
| 219 |
+
|
| 220 |
+
return 700
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
function animateCircle(direction) {
|
| 224 |
+
const boneR = "RightHand"
|
| 225 |
+
const boneL = "LeftHand"
|
| 226 |
+
const currentR = getCurrentRotation(boneR)
|
| 227 |
+
const currentL = getCurrentRotation(boneL)
|
| 228 |
+
const steps = 8
|
| 229 |
+
const radius = 0.2
|
| 230 |
+
let step = 0
|
| 231 |
+
|
| 232 |
+
const interval = setInterval(() => {
|
| 233 |
+
const angle = (step / steps) * Math.PI * 2 * direction
|
| 234 |
+
if (window.targetRotations[boneR]) {
|
| 235 |
+
window.targetRotations[boneR] = {
|
| 236 |
+
x: currentR.x + Math.sin(angle) * radius,
|
| 237 |
+
y: currentR.y + Math.cos(angle) * radius,
|
| 238 |
+
z: currentR.z
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
if (window.targetRotations[boneL]) {
|
| 242 |
+
// Mirror circle on left hand (invert Y)
|
| 243 |
+
window.targetRotations[boneL] = {
|
| 244 |
+
x: currentL.x + Math.sin(angle) * radius,
|
| 245 |
+
y: currentL.y - Math.cos(angle) * radius,
|
| 246 |
+
z: currentL.z
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
step++
|
| 250 |
+
if (step >= steps) {
|
| 251 |
+
clearInterval(interval)
|
| 252 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
|
| 253 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
|
| 254 |
+
}
|
| 255 |
+
}, 80)
|
| 256 |
+
|
| 257 |
+
return steps * 80 + 100
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
function animateForward() {
|
| 261 |
+
const boneR = "RightForeArm"
|
| 262 |
+
const boneL = "LeftForeArm"
|
| 263 |
+
const currentR = getCurrentRotation(boneR)
|
| 264 |
+
const currentL = getCurrentRotation(boneL)
|
| 265 |
+
|
| 266 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x - 0.3 }
|
| 267 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x - 0.3 }
|
| 268 |
+
|
| 269 |
+
setTimeout(() => {
|
| 270 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
|
| 271 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
|
| 272 |
+
}, 350)
|
| 273 |
+
|
| 274 |
+
return 500
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
function animatePullIn() {
|
| 278 |
+
const boneR = "RightForeArm"
|
| 279 |
+
const boneL = "LeftForeArm"
|
| 280 |
+
const currentR = getCurrentRotation(boneR)
|
| 281 |
+
const currentL = getCurrentRotation(boneL)
|
| 282 |
+
|
| 283 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x + 0.3 }
|
| 284 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x + 0.3 }
|
| 285 |
+
|
| 286 |
+
setTimeout(() => {
|
| 287 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
|
| 288 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
|
| 289 |
+
}, 350)
|
| 290 |
+
|
| 291 |
+
return 500
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
function animateDown() {
|
| 295 |
+
const boneR = "RightArm"
|
| 296 |
+
const boneL = "LeftArm"
|
| 297 |
+
const currentR = getCurrentRotation(boneR)
|
| 298 |
+
const currentL = getCurrentRotation(boneL)
|
| 299 |
+
|
| 300 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x + 0.3 }
|
| 301 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x + 0.3 }
|
| 302 |
+
|
| 303 |
+
setTimeout(() => {
|
| 304 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
|
| 305 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
|
| 306 |
+
}, 350)
|
| 307 |
+
|
| 308 |
+
return 500
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
function animateUp() {
|
| 312 |
+
const boneR = "RightArm"
|
| 313 |
+
const boneL = "LeftArm"
|
| 314 |
+
const currentR = getCurrentRotation(boneR)
|
| 315 |
+
const currentL = getCurrentRotation(boneL)
|
| 316 |
+
|
| 317 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x - 0.3 }
|
| 318 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x - 0.3 }
|
| 319 |
+
|
| 320 |
+
setTimeout(() => {
|
| 321 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
|
| 322 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
|
| 323 |
+
}, 350)
|
| 324 |
+
|
| 325 |
+
return 500
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
function animateSideToSide() {
|
| 329 |
+
const boneR = "RightHand"
|
| 330 |
+
const boneL = "LeftHand"
|
| 331 |
+
const currentR = getCurrentRotation(boneR)
|
| 332 |
+
const currentL = getCurrentRotation(boneL)
|
| 333 |
+
|
| 334 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, z: currentR.z - 0.2 }
|
| 335 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, z: currentL.z + 0.2 }
|
| 336 |
+
|
| 337 |
+
setTimeout(() => {
|
| 338 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, z: currentR.z + 0.2 }
|
| 339 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, z: currentL.z - 0.2 }
|
| 340 |
+
setTimeout(() => {
|
| 341 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
|
| 342 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
|
| 343 |
+
}, 200)
|
| 344 |
+
}, 200)
|
| 345 |
+
|
| 346 |
+
return 600
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
function animateNod() {
|
| 350 |
+
const boneR = "RightHand"
|
| 351 |
+
const boneL = "LeftHand"
|
| 352 |
+
const currentR = getCurrentRotation(boneR)
|
| 353 |
+
const currentL = getCurrentRotation(boneL)
|
| 354 |
+
|
| 355 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x + 0.25 }
|
| 356 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x + 0.25 }
|
| 357 |
+
|
| 358 |
+
setTimeout(() => {
|
| 359 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
|
| 360 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
|
| 361 |
+
}, 250)
|
| 362 |
+
|
| 363 |
+
return 450
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
function animateTwist() {
|
| 367 |
+
const boneR = "RightHand"
|
| 368 |
+
const boneL = "LeftHand"
|
| 369 |
+
const currentR = getCurrentRotation(boneR)
|
| 370 |
+
const currentL = getCurrentRotation(boneL)
|
| 371 |
+
|
| 372 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, y: currentR.y + 0.4 }
|
| 373 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, y: currentL.y - 0.4 }
|
| 374 |
+
|
| 375 |
+
setTimeout(() => {
|
| 376 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, y: currentR.y - 0.4 }
|
| 377 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, y: currentL.y + 0.4 }
|
| 378 |
+
setTimeout(() => {
|
| 379 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
|
| 380 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
|
| 381 |
+
}, 200)
|
| 382 |
+
}, 250)
|
| 383 |
+
|
| 384 |
+
return 650
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
function animateWave() {
|
| 388 |
+
const boneR = "RightHand"
|
| 389 |
+
const boneL = "LeftHand"
|
| 390 |
+
const currentR = getCurrentRotation(boneR)
|
| 391 |
+
const currentL = getCurrentRotation(boneL)
|
| 392 |
+
let step = 0
|
| 393 |
+
const steps = 6
|
| 394 |
+
|
| 395 |
+
const interval = setInterval(() => {
|
| 396 |
+
const val = Math.sin(step * 1.2) * 0.2
|
| 397 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, z: currentR.z + val }
|
| 398 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, z: currentL.z - val }
|
| 399 |
+
step++
|
| 400 |
+
if (step >= steps) {
|
| 401 |
+
clearInterval(interval)
|
| 402 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
|
| 403 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
|
| 404 |
+
}
|
| 405 |
+
}, 100)
|
| 406 |
+
|
| 407 |
+
return steps * 100 + 100
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
function animateTouch() {
|
| 411 |
+
const boneR = "RightForeArm"
|
| 412 |
+
const boneL = "LeftForeArm"
|
| 413 |
+
const currentR = getCurrentRotation(boneR)
|
| 414 |
+
const currentL = getCurrentRotation(boneL)
|
| 415 |
+
|
| 416 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = { ...currentR, x: currentR.x + 0.15 }
|
| 417 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = { ...currentL, x: currentL.x + 0.15 }
|
| 418 |
+
|
| 419 |
+
setTimeout(() => {
|
| 420 |
+
if (window.targetRotations[boneR]) window.targetRotations[boneR] = currentR
|
| 421 |
+
if (window.targetRotations[boneL]) window.targetRotations[boneL] = currentL
|
| 422 |
+
}, 200)
|
| 423 |
+
|
| 424 |
+
return 400
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
/* ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββ */
|
| 428 |
+
|
| 429 |
+
function getCurrentRotation(boneName) {
|
| 430 |
+
if (window.targetRotations && window.targetRotations[boneName]) {
|
| 431 |
+
return { ...window.targetRotations[boneName] }
|
| 432 |
+
}
|
| 433 |
+
return { x: 0, y: 0, z: 0 }
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
function returnToNeutral() {
|
| 437 |
+
if (window.clearPose) window.clearPose()
|
| 438 |
+
}
|