Coverage for apps/streamchat/streamchat.py: 81%
86 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 04:47 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 04:47 +0000
1"""
2StreamChat: Generate a video chat response.
3Starts an HTTP server to accept job submissions and monitor job status.
4"""
6import json
7import re
8import sys
9import os
10import logging
11import aiofiles
12import aiofiles.os
14from typing import override
15from typing import Dict
16from typing import Any
17from typing import List
19from http import HTTPStatus
21from quart import request
22from quart import jsonify
24from streamchat_job import StreamChatJob
26# Local relative imports
27sys.path.append("..") # noqa: E402
28sys.path.append("../..") # noqa: E402
30from streamwise_job import StreamWiseJob
31from streamwise_app import StreamWiseApp
32from streamwise_app import run_app
34from quart_utils import QuartReturn
37# Find latest chatXXX_prompt.jsonl file
38# matches chat001_prompt.jsonl, chat23_prompt.jsonl, etc.
39HISTORY_JSONL_PATTERN = re.compile(r"^chat\d+_prompt\.jsonl$")
42class StreamChatApp(StreamWiseApp):
43 """Quart app for StreamChat video chat generation."""
45 def __init__(self) -> None:
46 super().__init__("streamchat")
48 # Register chat route
49 route = self.app.route
51 @route("/chat/<job_id>", methods=["POST"])
52 async def chat(job_id: str) -> QuartReturn:
53 logging.info(f"Received chat request for job {job_id}.")
54 job = self.jobs.get(job_id, None)
55 if not job:
56 logging.error(f"Job {job_id} not found.")
57 return jsonify({
58 "status": "error",
59 "error": f"Job {job_id} not found."
60 }), HTTPStatus.NOT_FOUND
62 form = await request.form
63 files = await request.files
64 user_message = form.get("message", "").strip()
65 audio_message = files.get("audio")
66 if not user_message and not audio_message:
67 job.logger.error("No text or audio message provided.")
68 return jsonify({
69 "status": "error",
70 "error": "No user message or audio provided."
71 }), HTTPStatus.BAD_REQUEST
73 if audio_message:
74 self.logger.info("Transcribing audio message.")
75 audio_path = f"/tmp/{job_id}_audio_input.webm"
76 audio_message.save(audio_path) # TODO this may not work
77 user_message = await job.transcribe_audio(audio_path)
78 self.logger.info(f"Transcribed audio message: {user_message}")
80 try:
81 reply = await job.gen_chat(user_message)
82 response = {
83 "status": "ok"
84 }
85 response.update(reply)
86 return jsonify(response), HTTPStatus.OK
87 except Exception as ex:
88 job.logger.error(f"Error generating chat response: {ex}")
89 return jsonify({
90 "status": "error",
91 "error": str(ex)
92 }), HTTPStatus.INTERNAL_SERVER_ERROR
94 @route("/chat/<job_id>/history", methods=["GET"])
95 async def chat_history(
96 job_id: str
97 ) -> QuartReturn:
98 job = self.jobs.get(job_id, None)
99 if job:
100 history = await job.get_chat_history()
101 else:
102 job_path = f"{self.tmp_dir}/{job_id}"
103 history = await get_chat_history_from_file(job_path, job_id)
104 return jsonify({
105 "status": "ok",
106 "history": history
107 }), HTTPStatus.OK
109 @override
110 def create_job(
111 self,
112 job_id: str,
113 job_config: Dict[str, Any]
114 ) -> StreamWiseJob:
115 return StreamChatJob(
116 job_id=job_id,
117 config=job_config,
118 service_manager=self.service_manager
119 )
122async def get_chat_history_from_file(
123 job_path: str,
124 job_id: str
125) -> List[Dict[str, str]]:
126 """
127 Load chat history from local JSONL file.
128 """
129 if not await aiofiles.os.path.exists(job_path):
130 raise FileNotFoundError(f"Job path '{job_path}' does not exist.")
132 jsonl_files = [
133 file
134 for file in await aiofiles.os.listdir(job_path)
135 if HISTORY_JSONL_PATTERN.match(file)]
136 if not jsonl_files:
137 raise FileNotFoundError(f"History for '{job_id}' does not exist.")
139 jsonl_files.sort(
140 key=lambda f: os.path.getmtime(os.path.join(job_path, f)),
141 reverse=True)
142 latest_file = os.path.join(job_path, jsonl_files[0])
143 history = await parse_chat_history(latest_file)
144 return history
147async def parse_chat_history(
148 file_path: str
149) -> List[Dict[str, str]]:
150 """Parse chat history from a JSONL file."""
151 history: List[Dict[str, str]] = []
152 if not await aiofiles.os.path.exists(file_path):
153 return history
154 async with aiofiles.open(file_path, "r", encoding="utf-8") as jsonl_file:
155 async for line in jsonl_file:
156 msg = json.loads(line.strip())
157 history.append(msg)
158 return history
161if __name__ == "__main__":
162 run_app(
163 StreamChatApp,
164 tmp_dir="/tmp/streamchat",
165 log_files=[
166 "streamwise.log",
167 "streamchat.log"
168 ],
169 app_name="StreamChat",
170 )