Skip to content

Commit 064dc6b

Browse files
feat(vc): inline transcript from artifacts API and add keywords
1 parent 639259f commit 064dc6b

2 files changed

Lines changed: 52 additions & 88 deletions

File tree

shortcuts/vc/vc_notes.go

Lines changed: 39 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//
66
// Three mutually exclusive input modes (only one allowed per invocation):
77
// meeting-ids: meeting.get → note_id → note detail API
8-
// minute-tokens: minutes API → note detail + AI artifacts + transcript
8+
// minute-tokens: minutes API → note detail + AI artifacts (transcript inlined)
99
// calendar-event-ids: primary calendar → mget_instance_relation_info → meeting_id → meeting.get → note_id
1010

1111
package vc
@@ -44,7 +44,6 @@ var (
4444
scopesMinuteTokens = []string{
4545
"minutes:minutes:readonly",
4646
"minutes:minutes.artifacts:read",
47-
"minutes:minutes.transcript:export",
4847
}
4948
scopesCalendarEventIDs = []string{
5049
"calendar:calendar:read",
@@ -436,13 +435,9 @@ func fetchNoteByMinuteToken(ctx context.Context, runtime *common.RuntimeContext,
436435
}
437436
}
438437

439-
// path 2 & 3: AI artifacts are collected under the artifacts field.
438+
// AI artifacts + transcript come from the same /artifacts endpoint.
440439
artifacts := map[string]any{}
441-
fetchInlineArtifacts(runtime, minuteToken, artifacts)
442-
transcriptPath := downloadTranscriptFile(runtime, minuteToken, title)
443-
if transcriptPath != "" {
444-
artifacts["transcript_file"] = transcriptPath
445-
}
440+
fetchInlineArtifacts(runtime, minuteToken, title, artifacts)
446441
if len(artifacts) > 0 {
447442
result["artifacts"] = artifacts
448443
}
@@ -469,8 +464,39 @@ func sanitizeDirName(title, minuteToken string) string {
469464
return fmt.Sprintf("artifact-%s-%s", safe, minuteToken)
470465
}
471466

472-
// downloadTranscriptFile downloads transcript to a local file and returns the file path (empty on failure).
473-
func downloadTranscriptFile(runtime *common.RuntimeContext, minuteToken string, title string) string {
467+
// fetchInlineArtifacts fetches summary/todos/chapters/keywords and transcript from the
468+
// /artifacts API, persists transcript to disk, and exposes the path as transcript_file.
469+
func fetchInlineArtifacts(runtime *common.RuntimeContext, minuteToken string, title string, result map[string]any) {
470+
errOut := runtime.IO().ErrOut
471+
fmt.Fprintf(errOut, "%s fetching AI artifacts...\n", logPrefix)
472+
data, err := runtime.DoAPIJSON(http.MethodGet, fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/artifacts", validate.EncodePathSegment(minuteToken)), nil, nil)
473+
if err != nil {
474+
fmt.Fprintf(errOut, "%s failed to fetch AI artifacts: %v\n", logPrefix, err)
475+
return
476+
}
477+
if summary, ok := data["summary"].(string); ok && summary != "" {
478+
result["summary"] = summary
479+
}
480+
if todos, ok := data["minute_todos"].([]any); ok && len(todos) > 0 {
481+
result["todos"] = todos
482+
}
483+
if chapters, ok := data["minute_chapters"].([]any); ok && len(chapters) > 0 {
484+
result["chapters"] = chapters
485+
}
486+
if keywords, ok := data["keywords"].([]any); ok && len(keywords) > 0 {
487+
result["keywords"] = keywords
488+
}
489+
if transcript, ok := data["transcript"].(string); ok && transcript != "" {
490+
if path := saveTranscriptToFile(runtime, minuteToken, title, []byte(transcript)); path != "" {
491+
result["transcript_file"] = path
492+
}
493+
}
494+
}
495+
496+
// saveTranscriptToFile persists transcript bytes to the canonical artifact path
497+
// for the given minute_token. Returns the file path on success (or when the
498+
// file already exists and --overwrite is not set), empty string on any failure.
499+
func saveTranscriptToFile(runtime *common.RuntimeContext, minuteToken, title string, content []byte) string {
474500
errOut := runtime.IO().ErrOut
475501

476502
// With no --output-dir the default layout shares the directory with
@@ -483,37 +509,15 @@ func downloadTranscriptFile(runtime *common.RuntimeContext, minuteToken string,
483509
}
484510
transcriptPath := filepath.Join(dirName, common.DefaultTranscriptFileName)
485511

486-
// Overwrite check via FileIO.Stat
487512
if !runtime.Bool("overwrite") {
488513
if _, statErr := runtime.FileIO().Stat(transcriptPath); statErr == nil {
489514
fmt.Fprintf(errOut, "%s transcript already exists: %s (use --overwrite to replace)\n", logPrefix, transcriptPath)
490515
return transcriptPath
491516
}
492517
}
493518

494-
fmt.Fprintf(errOut, "%s downloading transcript: %s\n", logPrefix, transcriptPath)
495-
apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
496-
HttpMethod: http.MethodGet,
497-
ApiPath: fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/transcript", validate.EncodePathSegment(minuteToken)),
498-
QueryParams: larkcore.QueryParams{
499-
"need_speaker": []string{"true"},
500-
"need_timestamp": []string{"true"},
501-
"file_format": []string{"txt"},
502-
},
503-
}, larkcore.WithFileDownload())
504-
if err != nil {
505-
fmt.Fprintf(errOut, "%s failed to download transcript: %v\n", logPrefix, err)
506-
return ""
507-
}
508-
if apiResp.StatusCode >= 400 {
509-
fmt.Fprintf(errOut, "%s failed to download transcript: HTTP %d\n", logPrefix, apiResp.StatusCode)
510-
return ""
511-
}
512-
if len(apiResp.RawBody) == 0 {
513-
fmt.Fprintf(errOut, "%s transcript is empty (not available for this minute)\n", logPrefix)
514-
return ""
515-
}
516-
if _, err := runtime.FileIO().Save(transcriptPath, fileio.SaveOptions{}, bytes.NewReader(apiResp.RawBody)); err != nil {
519+
fmt.Fprintf(errOut, "%s writing transcript: %s\n", logPrefix, transcriptPath)
520+
if _, err := runtime.FileIO().Save(transcriptPath, fileio.SaveOptions{}, bytes.NewReader(content)); err != nil {
517521
var me *fileio.MkdirError
518522
switch {
519523
case errors.Is(err, fileio.ErrPathValidation):
@@ -528,29 +532,6 @@ func downloadTranscriptFile(runtime *common.RuntimeContext, minuteToken string,
528532
return transcriptPath
529533
}
530534

531-
// fetchInlineArtifacts fetches summary/todos/chapters from artifacts API and writes them inline into result map.
532-
func fetchInlineArtifacts(runtime *common.RuntimeContext, minuteToken string, result map[string]any) {
533-
errOut := runtime.IO().ErrOut
534-
fmt.Fprintf(errOut, "%s fetching AI artifacts...\n", logPrefix)
535-
data, err := runtime.DoAPIJSON(http.MethodGet, fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/artifacts", validate.EncodePathSegment(minuteToken)), nil, nil)
536-
if err != nil {
537-
fmt.Fprintf(errOut, "%s failed to fetch AI artifacts: %v\n", logPrefix, err)
538-
return
539-
}
540-
if summary, ok := data["summary"].(string); ok && summary != "" {
541-
result["summary"] = summary
542-
}
543-
if todos, ok := data["minute_todos"].([]any); ok && len(todos) > 0 {
544-
result["todos"] = todos
545-
}
546-
if chapters, ok := data["minute_chapters"].([]any); ok && len(chapters) > 0 {
547-
result["chapters"] = chapters
548-
}
549-
if keywords, ok := data["keywords"].([]any); ok && len(keywords) > 0 {
550-
result["keywords"] = keywords
551-
}
552-
}
553-
554535
// parseArtifactType extracts artifact_type as int from varying JSON number representations.
555536
func parseArtifactType(v any) int {
556537
switch n := v.(type) {
@@ -712,9 +693,8 @@ var VCNotes = common.Shortcut{
712693
GET("/open-apis/minutes/v1/minutes/{minute_token}").
713694
GET("/open-apis/vc/v1/notes/{note_id}").
714695
GET("/open-apis/minutes/v1/minutes/{minute_token}/artifacts").
715-
GET("/open-apis/minutes/v1/minutes/{minute_token}/transcript").
716696
Set("minute_tokens", common.SplitCSV(tokens)).
717-
Set("steps", "minutes API → note detail + AI artifacts + transcript")
697+
Set("steps", "minutes API → note detail + AI artifacts (incl. transcript)")
718698
}
719699
ids := runtime.Str("calendar-event-ids")
720700
return common.NewDryRunAPI().

shortcuts/vc/vc_notes_test.go

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -116,18 +116,22 @@ func noteDetailStub(noteID string) *httpmock.Stub {
116116
}
117117
}
118118

119-
func artifactsStub(token string) *httpmock.Stub {
119+
func artifactsStub(token, transcript string) *httpmock.Stub {
120+
data := map[string]interface{}{
121+
"summary": "Test summary content",
122+
"minute_todos": []interface{}{map[string]interface{}{"content": "Buy milk"}},
123+
"minute_chapters": []interface{}{map[string]interface{}{"title": "Intro", "summary_content": "Opening"}},
124+
"keywords": []interface{}{"budget", "roadmap"},
125+
}
126+
if transcript != "" {
127+
data["transcript"] = transcript
128+
}
120129
return &httpmock.Stub{
121130
Method: "GET",
122131
URL: "/open-apis/minutes/v1/minutes/" + token + "/artifacts",
123132
Body: map[string]interface{}{
124133
"code": 0, "msg": "ok",
125-
"data": map[string]interface{}{
126-
"summary": "Test summary content",
127-
"minute_todos": []interface{}{map[string]interface{}{"content": "Buy milk"}},
128-
"minute_chapters": []interface{}{map[string]interface{}{"title": "Intro", "summary_content": "Opening"}},
129-
"keywords": []interface{}{"budget", "roadmap"},
130-
},
134+
"data": data,
131135
},
132136
}
133137
}
@@ -140,24 +144,6 @@ func emptyArtifactsStub(token string) *httpmock.Stub {
140144
}
141145
}
142146

143-
func transcriptStub(token string) *httpmock.Stub {
144-
return &httpmock.Stub{
145-
Method: "GET",
146-
URL: "/open-apis/minutes/v1/minutes/" + token + "/transcript",
147-
Body: map[string]interface{}{"code": 0, "msg": "ok", "data": map[string]interface{}{}},
148-
}
149-
}
150-
151-
// transcriptRawStub returns an actual transcript body so downloadTranscriptFile
152-
// writes a file to disk. Used by path-layout tests.
153-
func transcriptRawStub(token string, body []byte) *httpmock.Stub {
154-
return &httpmock.Stub{
155-
Method: "GET",
156-
URL: "/open-apis/minutes/v1/minutes/" + token + "/transcript",
157-
RawBody: body,
158-
}
159-
}
160-
161147
func minuteGetStub(token, noteID, title string) *httpmock.Stub {
162148
minute := map[string]interface{}{"title": title}
163149
if noteID != "" {
@@ -677,8 +663,7 @@ func TestNotes_TranscriptDefaultLayout(t *testing.T) {
677663

678664
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
679665
reg.Register(minuteGetStub("tok001", "", "Meeting Title"))
680-
reg.Register(emptyArtifactsStub("tok001"))
681-
reg.Register(transcriptRawStub("tok001", []byte("speaker1: hello world\n")))
666+
reg.Register(artifactsStub("tok001", "speaker1: hello world\n"))
682667

683668
err := mountAndRun(t, VCNotes, []string{
684669
"+notes", "--minute-tokens", "tok001", "--as", "user",
@@ -706,8 +691,7 @@ func TestNotes_TranscriptExplicitOutputDir_PreservesLegacyLayout(t *testing.T) {
706691

707692
f, _, _, reg := cmdutil.TestFactory(t, defaultConfig())
708693
reg.Register(minuteGetStub("tok001", "", "Meeting Title"))
709-
reg.Register(emptyArtifactsStub("tok001"))
710-
reg.Register(transcriptRawStub("tok001", []byte("content")))
694+
reg.Register(artifactsStub("tok001", "content"))
711695

712696
if err := os.MkdirAll("out", 0755); err != nil {
713697
t.Fatalf("setup: %v", err)

0 commit comments

Comments
 (0)