Initial commit of current state
This commit is contained in:
+71
@@ -0,0 +1,71 @@
|
||||
"""Record what was said and what came back, one line of JSON per turn.
|
||||
|
||||
Every improvement so far has come from measuring something rather than guessing
|
||||
at it, and there is no record of real conversations to measure. This writes one
|
||||
so questions like "which words does it mishear most often" have an answer that
|
||||
isn't synthesised audio.
|
||||
|
||||
The two halves arrive by different routes, which is the awkward part. A
|
||||
transcript reaches this processor as a frame, but replies never do: the user
|
||||
aggregator *consumes* TranscriptionFrames rather than forwarding them, so a
|
||||
processor placed late enough to see LLMTextFrames is already too late to see
|
||||
the transcript. Sitting early and taking the reply through the LLM's own
|
||||
callback is the only placement that sees both.
|
||||
|
||||
Deliberately not in git: it is append-only, grows without bound, and is a
|
||||
verbatim record of everything said near the microphone.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from pipecat.frames.frames import CancelFrame, EndFrame, Frame, TranscriptionFrame
|
||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||
|
||||
|
||||
class Journal(FrameProcessor):
|
||||
"""Log each turn as JSONL. Place it just after the transcript repair."""
|
||||
|
||||
def __init__(self, path: Path, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._path = path
|
||||
self._heard: str | None = None
|
||||
|
||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||
await super().process_frame(frame, direction)
|
||||
|
||||
if isinstance(frame, TranscriptionFrame):
|
||||
# A second transcript before any reply means the previous turn was
|
||||
# abandoned — record it anyway, since an unanswered utterance is
|
||||
# exactly the kind of failure worth being able to find later.
|
||||
if self._heard:
|
||||
self._write(self._heard, None)
|
||||
self._heard = frame.text
|
||||
elif isinstance(frame, (EndFrame, CancelFrame)) and self._heard:
|
||||
self._write(self._heard, None)
|
||||
self._heard = None
|
||||
|
||||
await self.push_frame(frame, direction)
|
||||
|
||||
def record_reply(self, reply: str):
|
||||
"""Called by the LLM with each completed answer."""
|
||||
self._write(self._heard, reply)
|
||||
self._heard = None
|
||||
|
||||
def _write(self, heard: str | None, reply: str | None):
|
||||
if not heard and not reply:
|
||||
return
|
||||
entry = {
|
||||
"at": datetime.now().isoformat(timespec="seconds"),
|
||||
"heard": heard,
|
||||
"reply": reply,
|
||||
}
|
||||
try:
|
||||
self._path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self._path.open("a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
except OSError as e:
|
||||
logger.debug(f"Could not write the journal: {e}")
|
||||
Reference in New Issue
Block a user