Upload 2 files
Browse files- app.py +67 -3
- database.py +9 -0
app.py
CHANGED
|
@@ -148,8 +148,16 @@ def patient_input():
|
|
| 148 |
})
|
| 149 |
|
| 150 |
|
| 151 |
-
@app.route("/
|
| 152 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
interaction_id = request.form.get("interaction_id")
|
| 154 |
response_text = request.form.get("response_text")
|
| 155 |
|
|
@@ -163,10 +171,49 @@ def provider_response():
|
|
| 163 |
patient_language = interaction["detected_language"]
|
| 164 |
translated_response = translation.translate(response_text, "english", patient_language)
|
| 165 |
|
| 166 |
-
audio_filename = f"
|
| 167 |
audio_path = os.path.join(AUDIO_DIR, audio_filename)
|
| 168 |
tts.synthesize_speech(translated_response, patient_language, audio_path)
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
database.update_interaction_response(interaction_id, response_text, translated_response)
|
| 171 |
|
| 172 |
return jsonify({
|
|
@@ -193,6 +240,11 @@ def pending_interactions():
|
|
| 193 |
return jsonify(database.get_pending_interactions())
|
| 194 |
|
| 195 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
@app.route("/history")
|
| 197 |
def history():
|
| 198 |
if "user_id" not in session:
|
|
@@ -200,6 +252,18 @@ def history():
|
|
| 200 |
return jsonify(database.get_interactions_for_user(session["user_id"]))
|
| 201 |
|
| 202 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
@app.route("/end-session", methods=["POST"])
|
| 204 |
def end_session_route():
|
| 205 |
if "session_id" in session:
|
|
|
|
| 148 |
})
|
| 149 |
|
| 150 |
|
| 151 |
+
@app.route("/preview-response", methods=["POST"])
|
| 152 |
+
def preview_response():
|
| 153 |
+
"""
|
| 154 |
+
Runs the exact same translate + synthesize pipeline as
|
| 155 |
+
/provider-response below, so the provider can read and hear precisely
|
| 156 |
+
what the patient will receive before committing to it. Unlike
|
| 157 |
+
/provider-response, this never touches the database - only "Send
|
| 158 |
+
reply" actually saves a response. Each preview click is a real
|
| 159 |
+
translation + speech synthesis call, same cost as an actual send.
|
| 160 |
+
"""
|
| 161 |
interaction_id = request.form.get("interaction_id")
|
| 162 |
response_text = request.form.get("response_text")
|
| 163 |
|
|
|
|
| 171 |
patient_language = interaction["detected_language"]
|
| 172 |
translated_response = translation.translate(response_text, "english", patient_language)
|
| 173 |
|
| 174 |
+
audio_filename = f"preview_{interaction_id}.wav"
|
| 175 |
audio_path = os.path.join(AUDIO_DIR, audio_filename)
|
| 176 |
tts.synthesize_speech(translated_response, patient_language, audio_path)
|
| 177 |
|
| 178 |
+
return jsonify({
|
| 179 |
+
"translated_response": translated_response,
|
| 180 |
+
"audio_url": f"/static/audio/{audio_filename}",
|
| 181 |
+
})
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
@app.route("/provider-response", methods=["POST"])
|
| 185 |
+
def provider_response():
|
| 186 |
+
interaction_id = request.form.get("interaction_id")
|
| 187 |
+
response_text = request.form.get("response_text")
|
| 188 |
+
# Optional cache-reuse fields: if the provider already generated a
|
| 189 |
+
# preview of this exact reply via /preview-response and didn't edit
|
| 190 |
+
# it afterward, the frontend passes the already-computed translation
|
| 191 |
+
# back here so this route can skip re-translating and re-synthesizing
|
| 192 |
+
# speech. Both are best-effort - if the preview audio isn't actually
|
| 193 |
+
# on disk, this quietly falls back to doing the full work itself,
|
| 194 |
+
# exactly as if these fields had never been sent.
|
| 195 |
+
cached_translation = request.form.get("translated_response")
|
| 196 |
+
reuse_audio = request.form.get("reuse_audio") == "true"
|
| 197 |
+
|
| 198 |
+
if not interaction_id or not response_text:
|
| 199 |
+
return jsonify({"error": "interaction_id and response_text are required"}), 400
|
| 200 |
+
|
| 201 |
+
interaction = database.get_interaction(interaction_id)
|
| 202 |
+
if not interaction:
|
| 203 |
+
return jsonify({"error": "interaction not found"}), 404
|
| 204 |
+
|
| 205 |
+
patient_language = interaction["detected_language"]
|
| 206 |
+
audio_filename = f"response_{interaction_id}.wav"
|
| 207 |
+
audio_path = os.path.join(AUDIO_DIR, audio_filename)
|
| 208 |
+
preview_path = os.path.join(AUDIO_DIR, f"preview_{interaction_id}.wav")
|
| 209 |
+
|
| 210 |
+
if cached_translation and reuse_audio and os.path.exists(preview_path):
|
| 211 |
+
translated_response = cached_translation
|
| 212 |
+
os.replace(preview_path, audio_path)
|
| 213 |
+
else:
|
| 214 |
+
translated_response = translation.translate(response_text, "english", patient_language)
|
| 215 |
+
tts.synthesize_speech(translated_response, patient_language, audio_path)
|
| 216 |
+
|
| 217 |
database.update_interaction_response(interaction_id, response_text, translated_response)
|
| 218 |
|
| 219 |
return jsonify({
|
|
|
|
| 240 |
return jsonify(database.get_pending_interactions())
|
| 241 |
|
| 242 |
|
| 243 |
+
@app.route("/completed-interactions")
|
| 244 |
+
def completed_interactions():
|
| 245 |
+
return jsonify(database.get_completed_interactions())
|
| 246 |
+
|
| 247 |
+
|
| 248 |
@app.route("/history")
|
| 249 |
def history():
|
| 250 |
if "user_id" not in session:
|
|
|
|
| 252 |
return jsonify(database.get_interactions_for_user(session["user_id"]))
|
| 253 |
|
| 254 |
|
| 255 |
+
@app.route("/patient-history/<user_id>")
|
| 256 |
+
def patient_history(user_id):
|
| 257 |
+
"""
|
| 258 |
+
All of one patient's interactions, for the provider workspace's
|
| 259 |
+
"conversation history" panel. Distinct from /history above: that route
|
| 260 |
+
always looks up the current browser's own session, which only works
|
| 261 |
+
for the patient viewing their own messages. The provider needs to look
|
| 262 |
+
up a specific patient's history by id instead.
|
| 263 |
+
"""
|
| 264 |
+
return jsonify(database.get_interactions_for_user(user_id))
|
| 265 |
+
|
| 266 |
+
|
| 267 |
@app.route("/end-session", methods=["POST"])
|
| 268 |
def end_session_route():
|
| 269 |
if "session_id" in session:
|
database.py
CHANGED
|
@@ -114,6 +114,15 @@ def get_pending_interactions():
|
|
| 114 |
return [dict(row) for row in cursor.fetchall()]
|
| 115 |
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
def get_interaction(interaction_id):
|
| 118 |
with get_connection() as connection:
|
| 119 |
cursor = _run(connection, "SELECT * FROM interaction WHERE id = ?", (interaction_id,))
|
|
|
|
| 114 |
return [dict(row) for row in cursor.fetchall()]
|
| 115 |
|
| 116 |
|
| 117 |
+
def get_completed_interactions():
|
| 118 |
+
with get_connection() as connection:
|
| 119 |
+
cursor = _run(
|
| 120 |
+
connection,
|
| 121 |
+
"SELECT * FROM interaction WHERE provider_response IS NOT NULL ORDER BY timestamp DESC",
|
| 122 |
+
)
|
| 123 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 124 |
+
|
| 125 |
+
|
| 126 |
def get_interaction(interaction_id):
|
| 127 |
with get_connection() as connection:
|
| 128 |
cursor = _run(connection, "SELECT * FROM interaction WHERE id = ?", (interaction_id,))
|