capture: answer the stop before summarising, and always leave a note
capture_stop held the IPC request open for the whole map reduce, up to twenty minutes. A voice turn that says "хватит" waited for forty model calls before Maven said anything. Stop now returns the transcript and the summary runs on a goroutine in the daemon's WaitGroup, on the daemon context so a client that hung up does not cancel the only readable record of the meeting. With no summary and save_transcript false, writeNotes wrote nothing at all: an hour of meeting left a blob that prunes in seven days and no trace in the note store. The transcript is written instead when the summary is missing. That flag decides whether the verbatim record is kept in addition to a summary, not whether the meeting is remembered. The wire carries the session token now, and the contract comments say what the code does: the summary is usually absent from the stop response, and re running a stored blob is a manual job because no method takes a blob id. The save_transcript comment says the cost is recall corpus rather than disk. Found in review of #73.
This commit is contained in:
@@ -27,8 +27,8 @@ import (
|
||||
"github.com/kami/maven/internal/morning"
|
||||
"github.com/kami/maven/internal/netscan"
|
||||
"github.com/kami/maven/internal/smarthome"
|
||||
"github.com/kami/maven/internal/vision"
|
||||
"github.com/kami/maven/internal/update"
|
||||
"github.com/kami/maven/internal/vision"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
@@ -787,9 +787,14 @@ type CaptureConfig struct {
|
||||
MaxChunks int `json:"max_chunks,omitempty"`
|
||||
|
||||
// SaveTranscript — write the full transcript as a note alongside the
|
||||
// summary. Default false: a verbatim record of what other people said in a
|
||||
// room is a heavier thing to keep than a four-line summary, so it takes a
|
||||
// deliberate yes. The audio blob is pruned by media.retention either way.
|
||||
// summary. Default false, and the cost is not disk: a note is embedded and
|
||||
// becomes recall corpus, so every later question can surface verbatim words
|
||||
// other people said in a room. That is the reason it takes a deliberate yes.
|
||||
// The audio blob is pruned by media.retention either way; the notes are not.
|
||||
//
|
||||
// A meeting with no summary writes its transcript regardless. The choice
|
||||
// here is transcript IN ADDITION to a summary, not whether the meeting is
|
||||
// remembered at all.
|
||||
SaveTranscript bool `json:"save_transcript,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -228,14 +228,14 @@ func TestSpeakerBlockParsesFromJSON(t *testing.T) {
|
||||
// both fail at startup now.
|
||||
func TestSensesBlocksAreValidatedAtStartup(t *testing.T) {
|
||||
bad := map[string]string{
|
||||
"media with no dir": `{"media":{"retention":"48h"}}`,
|
||||
"negative budget": `{"media":{"dir":"/srv/media","max_total_bytes":-1}}`,
|
||||
"blob over the budget": `{"media":{"dir":"/srv/media","max_bytes":100,"max_total_bytes":10}}`,
|
||||
"vision with no media dir": `{"vision":{"enabled":true,"endpoint":"http://127.0.0.1:8081"}}`,
|
||||
"vision endpoint typo": `{"media":{"dir":"/srv/media"},"vision":{"enabled":true,"endpoint":"127.0.0.1:8081"}}`,
|
||||
"vision on the wan": `{"media":{"dir":"/srv/media"},"vision":{"enabled":true,"endpoint":"http://8.8.8.8:8081"}}`,
|
||||
"vision, empty endpoint": `{"media":{"dir":"/srv/media"},"vision":{"enabled":true}}`,
|
||||
"capture with no store": `{"capture":{"enabled":true}}`,
|
||||
"media with no dir": `{"media":{"retention":"48h"}}`,
|
||||
"negative budget": `{"media":{"dir":"/srv/media","max_total_bytes":-1}}`,
|
||||
"blob over the budget": `{"media":{"dir":"/srv/media","max_bytes":100,"max_total_bytes":10}}`,
|
||||
"vision with no media dir": `{"vision":{"enabled":true,"endpoint":"http://127.0.0.1:8081"}}`,
|
||||
"vision endpoint typo": `{"media":{"dir":"/srv/media"},"vision":{"enabled":true,"endpoint":"127.0.0.1:8081"}}`,
|
||||
"vision on the wan": `{"media":{"dir":"/srv/media"},"vision":{"enabled":true,"endpoint":"http://8.8.8.8:8081"}}`,
|
||||
"vision, empty endpoint": `{"media":{"dir":"/srv/media"},"vision":{"enabled":true}}`,
|
||||
"capture with no store": `{"capture":{"enabled":true}}`,
|
||||
}
|
||||
for name, body := range bad {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
||||
+22
-7
@@ -247,9 +247,14 @@ type CaptureStartReq struct {
|
||||
// which it stops itself; the caller tells him, so a forgotten recording is his
|
||||
// own informed choice rather than a surprise.
|
||||
type CaptureStartResp struct {
|
||||
Label string `json:"label,omitempty"`
|
||||
Started time.Time `json:"started"`
|
||||
MaxSeconds int `json:"max_seconds"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Started time.Time `json:"started"`
|
||||
// Token names THIS session. Every later append, stop and discard has to
|
||||
// carry it. Without it the recorder is addressed by "whatever is running
|
||||
// now", and a client whose session already ended on the duration cap goes on
|
||||
// appending its microphone into the next session someone else started.
|
||||
Token string `json:"token"`
|
||||
MaxSeconds int `json:"max_seconds"`
|
||||
}
|
||||
|
||||
// CaptureAppendReq — one chunk of audio for the running session. Refused with
|
||||
@@ -257,6 +262,9 @@ type CaptureStartResp struct {
|
||||
// makes an ambient path impossible: audio arriving at an idle core is dropped on
|
||||
// the floor, not buffered "just in case".
|
||||
type CaptureAppendReq struct {
|
||||
// Token from CaptureStartResp. A frame for a session that already ended is
|
||||
// refused rather than folded into whatever is running now.
|
||||
Token string `json:"token"`
|
||||
Audio audio.Audio `json:"audio"`
|
||||
}
|
||||
|
||||
@@ -275,15 +283,22 @@ type CaptureAppendResp struct {
|
||||
// flag rather than a separate method so the client that says "stop" and the
|
||||
// client that says "stop and forget" take the same path to the same session.
|
||||
type CaptureStopReq struct {
|
||||
Discard bool `json:"discard,omitempty"`
|
||||
// Token from CaptureStartResp. Stopping by "whatever is running" lets a
|
||||
// late client end a recording it never started.
|
||||
Token string `json:"token"`
|
||||
Discard bool `json:"discard,omitempty"`
|
||||
}
|
||||
|
||||
// CaptureStopResp — the finished capture. BlobID is the stored WAV, kept under
|
||||
// media.retention like any other blob and pruned with it.
|
||||
//
|
||||
// A response with a Transcript and an empty Summary is a degraded success: the
|
||||
// words exist, only the model failed. A response with a BlobID and neither is
|
||||
// the audio surviving a transcription failure — the same id can be run again.
|
||||
// A response with a Transcript and an empty Summary is the normal shape, not a
|
||||
// failure: summarising a long meeting is a map-reduce of minutes, so stop
|
||||
// answers with the words and the summary note is written afterwards. Summary is
|
||||
// filled in only when it happened to be ready. A response with a BlobID and no
|
||||
// transcript is the audio surviving a transcription failure — the same id can be
|
||||
// run again by hand off the blob before media.retention prunes it — there is no
|
||||
// capture method that takes a blob id, so this is not a re-run the wire offers.
|
||||
// Discarded is true when nothing was kept.
|
||||
type CaptureStopResp struct {
|
||||
BlobID string `json:"blob_id,omitempty"`
|
||||
|
||||
@@ -500,9 +500,10 @@ func (c *Client) CaptureAppend(ctx context.Context, req CaptureAppendReq) (Captu
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// CaptureStop ends the session. Slow — it transcribes and summarises the whole
|
||||
// recording — so pass a context with room. Set Discard to throw the recording
|
||||
// away instead.
|
||||
// CaptureStop ends the session. It transcribes the whole recording before
|
||||
// answering, so pass a context with room; the summary is written afterwards by
|
||||
// the daemon and is usually absent from the response. Set Discard to throw the
|
||||
// recording away instead. Token comes from CaptureStart.
|
||||
func (c *Client) CaptureStop(ctx context.Context, req CaptureStopReq) (CaptureStopResp, error) {
|
||||
var r CaptureStopResp
|
||||
if err := c.call(ctx, MethodCaptureStop, req, &r); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user