carlosduplar commited on
Commit ·
3ab287a
1
Parent(s): a31d332
Restructure repo: src/, frontend/, modal/ dirs; update app_file paths; update .gitignore
Browse files- .gitignore +26 -8
- README.md +19 -10
- custom_index.html → frontend/custom_index.html +0 -0
- style.css → frontend/style.css +0 -0
- index.html +0 -13
- metadata.json +0 -5
- modal_app.py → modal/modal_app.py +0 -0
- space_README.md +22 -1
- src/__init__.py +0 -0
- core.py → src/core.py +0 -0
- llm_engine.py → src/llm_engine.py +0 -0
- parse_feedback.py → src/parse_feedback.py +0 -0
- prompts.py → src/prompts.py +0 -0
- server_app.py → src/server_app.py +5 -2
- stt_engine.py → src/stt_engine.py +0 -0
- tts_engine.py → src/tts_engine.py +0 -0
- vite.config.ts +0 -24
.gitignore
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
node_modules/
|
| 2 |
-
|
|
|
|
| 3 |
dist/
|
|
|
|
| 4 |
coverage/
|
| 5 |
-
|
| 6 |
-
|
| 7 |
.env*
|
| 8 |
!.env.example
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
.
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
legacy/
|
| 14 |
experimental/
|
| 15 |
-
*.swp
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
.gradio/
|
| 6 |
+
|
| 7 |
+
# Node
|
| 8 |
node_modules/
|
| 9 |
+
|
| 10 |
+
# Build artifacts
|
| 11 |
dist/
|
| 12 |
+
build/
|
| 13 |
coverage/
|
| 14 |
+
|
| 15 |
+
# Environment
|
| 16 |
.env*
|
| 17 |
!.env.example
|
| 18 |
+
|
| 19 |
+
# OS
|
| 20 |
+
.DS_Store
|
| 21 |
+
*.swp
|
| 22 |
+
|
| 23 |
+
# Logs
|
| 24 |
+
*.log
|
| 25 |
+
|
| 26 |
+
# Data / temp
|
| 27 |
+
trace.jsonl
|
| 28 |
+
metadata.json
|
| 29 |
+
*.mp4
|
| 30 |
+
|
| 31 |
+
# Legacy / experiments
|
| 32 |
legacy/
|
| 33 |
experimental/
|
|
|
README.md
CHANGED
|
@@ -5,12 +5,13 @@ colorFrom: red
|
|
| 5 |
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.16.0
|
| 8 |
-
app_file: server_app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
| 11 |
tags:
|
| 12 |
- hackathon
|
| 13 |
- build-small-hackathon
|
|
|
|
| 14 |
- voice-ai
|
| 15 |
- french-learning
|
| 16 |
- dental
|
|
@@ -18,6 +19,8 @@ tags:
|
|
| 18 |
- modal
|
| 19 |
- llama-cpp
|
| 20 |
- gemma
|
|
|
|
|
|
|
| 21 |
---
|
| 22 |
|
| 23 |
# Patient Virtuel · Français en médecine dentaire
|
|
@@ -37,18 +40,24 @@ See [`space_README.md`](space_README.md) for the full description.
|
|
| 37 |
📖 [Field Notes](FIELD_NOTES.md) — architecture decisions, lessons learned, and the story behind the app.
|
| 38 |
📡 [Agent Trace](https://huggingface.co/datasets/build-small-hackathon/patient-virtuel-trace) — curated build log on HF Hub (Sharing is Caring badge).
|
| 39 |
|
|
|
|
|
|
|
| 40 |
## Architecture
|
| 41 |
|
| 42 |
```
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
core.py → Shared session logic
|
| 46 |
-
llm_engine.py → Gemma 4 26B-A4B via Modal (llama.cpp runtime)
|
| 47 |
-
stt_engine.py → faster-whisper via Modal
|
| 48 |
-
tts_engine.py → piper-tts (local neural, fr_FR-siwis-medium)
|
| 49 |
-
prompts.py → System prompts
|
| 50 |
-
parse_feedback.py → Feedback parsing
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
```
|
| 53 |
|
| 54 |
## Running locally
|
|
|
|
| 5 |
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.16.0
|
| 8 |
+
app_file: src/server_app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
| 11 |
tags:
|
| 12 |
- hackathon
|
| 13 |
- build-small-hackathon
|
| 14 |
+
- backyard-ai
|
| 15 |
- voice-ai
|
| 16 |
- french-learning
|
| 17 |
- dental
|
|
|
|
| 19 |
- modal
|
| 20 |
- llama-cpp
|
| 21 |
- gemma
|
| 22 |
+
- off-brand
|
| 23 |
+
- off-the-grid
|
| 24 |
---
|
| 25 |
|
| 26 |
# Patient Virtuel · Français en médecine dentaire
|
|
|
|
| 40 |
📖 [Field Notes](FIELD_NOTES.md) — architecture decisions, lessons learned, and the story behind the app.
|
| 41 |
📡 [Agent Trace](https://huggingface.co/datasets/build-small-hackathon/patient-virtuel-trace) — curated build log on HF Hub (Sharing is Caring badge).
|
| 42 |
|
| 43 |
+
[Demo video](https://youtu.be/oP3KrlZFUPk) · [LinkedIn post](https://www.linkedin.com/posts/carlosduplar_my-wife-is-a-dentist-re-entering-the-workforce-activity-7471897489332289536-p5Ne)
|
| 44 |
+
|
| 45 |
## Architecture
|
| 46 |
|
| 47 |
```
|
| 48 |
+
src/
|
| 49 |
+
server_app.py → gr.Server backend (API endpoints)
|
| 50 |
+
core.py → Shared session logic
|
| 51 |
+
llm_engine.py → Gemma 4 26B-A4B via Modal (llama.cpp runtime)
|
| 52 |
+
stt_engine.py → faster-whisper via Modal
|
| 53 |
+
tts_engine.py → piper-tts (local neural, fr_FR-siwis-medium)
|
| 54 |
+
prompts.py → System prompts
|
| 55 |
+
parse_feedback.py → Feedback parsing
|
| 56 |
+
frontend/
|
| 57 |
+
custom_index.html → Custom vanilla HTML/CSS/JS frontend
|
| 58 |
+
style.css → Styles
|
| 59 |
+
modal/
|
| 60 |
+
modal_app.py → Modal deployments (llama.cpp + CUDA)
|
| 61 |
```
|
| 62 |
|
| 63 |
## Running locally
|
custom_index.html → frontend/custom_index.html
RENAMED
|
File without changes
|
style.css → frontend/style.css
RENAMED
|
File without changes
|
index.html
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
<!doctype html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8" />
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
-
<title>My Google AI Studio App</title>
|
| 7 |
-
</head>
|
| 8 |
-
<body>
|
| 9 |
-
<div id="root"></div>
|
| 10 |
-
<script type="module" src="/src/main.tsx"></script>
|
| 11 |
-
</body>
|
| 12 |
-
</html>
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
metadata.json
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "Patient Virtuel: Français en médecine dentaire",
|
| 3 |
-
"description": "A real-time voice-only interactive app for dental hygienists to practice professional French in a Swiss clinic setting.",
|
| 4 |
-
"requestFramePermissions": ["microphone"]
|
| 5 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modal_app.py → modal/modal_app.py
RENAMED
|
File without changes
|
space_README.md
CHANGED
|
@@ -6,9 +6,22 @@ colorTo: gray
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.16.0
|
| 8 |
python_version: "3.12"
|
| 9 |
-
app_file: server_app.py
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
# Patient Virtuel · Français en médecine dentaire
|
|
@@ -34,6 +47,14 @@ This app uses `gradio.Server` with a fully custom HTML/CSS/JS frontend instead o
|
|
| 34 |
- **TTS**: [piper-tts](https://github.com/OHF-Voice/piper1-gpl) (local, `fr_FR-siwis-medium` voice)
|
| 35 |
- **STT**: [faster-whisper-large-v3-turbo](https://github.com/SYSTRAN/faster-whisper) via Modal (T4 GPU) — MIT
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
## License
|
| 38 |
|
| 39 |
All components are Apache 2.0 or MIT licensed.
|
|
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.16.0
|
| 8 |
python_version: "3.12"
|
| 9 |
+
app_file: src/server_app.py
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
| 12 |
+
tags:
|
| 13 |
+
- hackathon
|
| 14 |
+
- build-small-hackathon
|
| 15 |
+
- backyard-ai
|
| 16 |
+
- voice-ai
|
| 17 |
+
- french-learning
|
| 18 |
+
- dental
|
| 19 |
+
- gradio
|
| 20 |
+
- modal
|
| 21 |
+
- llama-cpp
|
| 22 |
+
- gemma
|
| 23 |
+
- off-brand
|
| 24 |
+
- off-the-grid
|
| 25 |
---
|
| 26 |
|
| 27 |
# Patient Virtuel · Français en médecine dentaire
|
|
|
|
| 47 |
- **TTS**: [piper-tts](https://github.com/OHF-Voice/piper1-gpl) (local, `fr_FR-siwis-medium` voice)
|
| 48 |
- **STT**: [faster-whisper-large-v3-turbo](https://github.com/SYSTRAN/faster-whisper) via Modal (T4 GPU) — MIT
|
| 49 |
|
| 50 |
+
## Modal
|
| 51 |
+
|
| 52 |
+
Both GPU workloads (LLM + STT) run on Modal serverless containers. See `modal_app.py` for the full deployment config.
|
| 53 |
+
|
| 54 |
+
## Social
|
| 55 |
+
|
| 56 |
+
[Demo video](https://youtu.be/oP3KrlZFUPk) · [LinkedIn post](https://www.linkedin.com/posts/carlosduplar_my-wife-is-a-dentist-re-entering-the-workforce-activity-7471897489332289536-p5Ne)
|
| 57 |
+
|
| 58 |
## License
|
| 59 |
|
| 60 |
All components are Apache 2.0 or MIT licensed.
|
src/__init__.py
ADDED
|
File without changes
|
core.py → src/core.py
RENAMED
|
File without changes
|
llm_engine.py → src/llm_engine.py
RENAMED
|
File without changes
|
parse_feedback.py → src/parse_feedback.py
RENAMED
|
File without changes
|
prompts.py → src/prompts.py
RENAMED
|
File without changes
|
server_app.py → src/server_app.py
RENAMED
|
@@ -1,7 +1,10 @@
|
|
| 1 |
import logging
|
| 2 |
import os
|
|
|
|
| 3 |
import tempfile
|
| 4 |
|
|
|
|
|
|
|
| 5 |
from gradio import Server
|
| 6 |
from gradio.data_classes import FileData
|
| 7 |
from fastapi.responses import HTMLResponse, FileResponse
|
|
@@ -100,7 +103,7 @@ async def serve_audio(filename: str):
|
|
| 100 |
|
| 101 |
@app.get("/", response_class=HTMLResponse)
|
| 102 |
async def homepage():
|
| 103 |
-
html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "custom_index.html")
|
| 104 |
with open(html_path, "r", encoding="utf-8") as f:
|
| 105 |
return f.read()
|
| 106 |
|
|
@@ -149,7 +152,7 @@ _FIELD_NOTES_HTML_TEMPLATE = """\
|
|
| 149 |
|
| 150 |
@app.get("/field-notes", response_class=HTMLResponse)
|
| 151 |
async def field_notes():
|
| 152 |
-
notes_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "FIELD_NOTES.md")
|
| 153 |
try:
|
| 154 |
with open(notes_path, "r", encoding="utf-8") as f:
|
| 155 |
raw = f.read()
|
|
|
|
| 1 |
import logging
|
| 2 |
import os
|
| 3 |
+
import sys
|
| 4 |
import tempfile
|
| 5 |
|
| 6 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 7 |
+
|
| 8 |
from gradio import Server
|
| 9 |
from gradio.data_classes import FileData
|
| 10 |
from fastapi.responses import HTMLResponse, FileResponse
|
|
|
|
| 103 |
|
| 104 |
@app.get("/", response_class=HTMLResponse)
|
| 105 |
async def homepage():
|
| 106 |
+
html_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "frontend", "custom_index.html")
|
| 107 |
with open(html_path, "r", encoding="utf-8") as f:
|
| 108 |
return f.read()
|
| 109 |
|
|
|
|
| 152 |
|
| 153 |
@app.get("/field-notes", response_class=HTMLResponse)
|
| 154 |
async def field_notes():
|
| 155 |
+
notes_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "FIELD_NOTES.md")
|
| 156 |
try:
|
| 157 |
with open(notes_path, "r", encoding="utf-8") as f:
|
| 158 |
raw = f.read()
|
stt_engine.py → src/stt_engine.py
RENAMED
|
File without changes
|
tts_engine.py → src/tts_engine.py
RENAMED
|
File without changes
|
vite.config.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
import tailwindcss from '@tailwindcss/vite';
|
| 2 |
-
import react from '@vitejs/plugin-react';
|
| 3 |
-
import path from 'path';
|
| 4 |
-
import {defineConfig, loadEnv} from 'vite';
|
| 5 |
-
|
| 6 |
-
export default defineConfig(({mode}) => {
|
| 7 |
-
const env = loadEnv(mode, '.', '');
|
| 8 |
-
return {
|
| 9 |
-
plugins: [react(), tailwindcss()],
|
| 10 |
-
define: {
|
| 11 |
-
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
| 12 |
-
},
|
| 13 |
-
resolve: {
|
| 14 |
-
alias: {
|
| 15 |
-
'@': path.resolve(__dirname, '.'),
|
| 16 |
-
},
|
| 17 |
-
},
|
| 18 |
-
server: {
|
| 19 |
-
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
| 20 |
-
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
| 21 |
-
hmr: process.env.DISABLE_HMR !== 'true',
|
| 22 |
-
},
|
| 23 |
-
};
|
| 24 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|