ffx-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ffx/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
ffx/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from ffx.commands import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
ffx/api_client.py ADDED
@@ -0,0 +1,261 @@
1
+ from __future__ import annotations
2
+
3
+ from gql import Client, gql
4
+ from gql.transport.httpx import HTTPXTransport
5
+
6
+ from ffx.models import (
7
+ ActionItem,
8
+ Sentence,
9
+ Speaker,
10
+ Summary,
11
+ Topic,
12
+ Transcript,
13
+ )
14
+
15
+ FIREFLIES_API_URL = "https://api.fireflies.ai/graphql"
16
+
17
+ # participants is [String!] (emails), not objects
18
+ # analytics.speakers is the correct path (not speaker_talk_time_percentage)
19
+ LIST_TRANSCRIPTS_QUERY = gql("""
20
+ query Transcripts($limit: Int, $skip: Int, $keyword: String, $fromDate: DateTime, $toDate: DateTime, $participants: [String!]) {
21
+ transcripts(limit: $limit, skip: $skip, keyword: $keyword, fromDate: $fromDate, toDate: $toDate, participants: $participants) {
22
+ id
23
+ title
24
+ date
25
+ duration
26
+ organizer_email
27
+ participants
28
+ summary {
29
+ action_items keywords overview gist bullet_gist
30
+ shorthand_bullet outline short_summary short_overview
31
+ meeting_type topics_discussed
32
+ }
33
+ analytics {
34
+ speakers {
35
+ speaker_id name duration duration_pct word_count
36
+ words_per_minute filler_words questions
37
+ longest_monologue monologues_count
38
+ }
39
+ }
40
+ }
41
+ }
42
+ """)
43
+
44
+ GET_TRANSCRIPT_QUERY = gql("""
45
+ query Transcript($id: String!) {
46
+ transcript(id: $id) {
47
+ id
48
+ title
49
+ date
50
+ duration
51
+ organizer_email
52
+ participants
53
+ summary {
54
+ action_items keywords overview gist bullet_gist
55
+ shorthand_bullet outline short_summary short_overview
56
+ meeting_type topics_discussed
57
+ }
58
+ analytics {
59
+ speakers {
60
+ speaker_id name duration duration_pct word_count
61
+ words_per_minute filler_words questions
62
+ longest_monologue monologues_count
63
+ }
64
+ }
65
+ }
66
+ }
67
+ """)
68
+
69
+
70
+ GET_TRANSCRIPT_WITH_SENTENCES_QUERY = gql("""
71
+ query Transcript($id: String!) {
72
+ transcript(id: $id) {
73
+ id
74
+ title
75
+ date
76
+ duration
77
+ organizer_email
78
+ participants
79
+ summary {
80
+ action_items keywords overview gist bullet_gist
81
+ shorthand_bullet outline short_summary short_overview
82
+ meeting_type topics_discussed
83
+ }
84
+ analytics {
85
+ speakers {
86
+ speaker_id name duration duration_pct word_count
87
+ words_per_minute filler_words questions
88
+ longest_monologue monologues_count
89
+ }
90
+ }
91
+ sentences {
92
+ index
93
+ speaker_name
94
+ speaker_id
95
+ text
96
+ raw_text
97
+ start_time
98
+ end_time
99
+ }
100
+ }
101
+ }
102
+ """)
103
+
104
+
105
+ class AuthError(Exception):
106
+ pass
107
+
108
+
109
+ class ApiError(Exception):
110
+ pass
111
+
112
+
113
+ def _parse_transcript(raw: dict) -> Transcript:
114
+ summary_raw = raw.get("summary") or {}
115
+ analytics_raw = raw.get("analytics") or {}
116
+
117
+ # Most summary fields come back as strings (with newlines), not lists.
118
+ # Only keywords is a proper list. We store raw and use .action_items_list etc.
119
+ summary = Summary(
120
+ overview=summary_raw.get("overview"),
121
+ short_overview=summary_raw.get("short_overview"),
122
+ gist=summary_raw.get("gist"),
123
+ short_summary=summary_raw.get("short_summary"),
124
+ bullet_gist=summary_raw.get("bullet_gist"),
125
+ shorthand_bullet=summary_raw.get("shorthand_bullet"),
126
+ action_items=summary_raw.get("action_items"),
127
+ keywords=summary_raw.get("keywords") or [],
128
+ topics_discussed=summary_raw.get("topics_discussed"),
129
+ outline=summary_raw.get("outline"),
130
+ meeting_type=summary_raw.get("meeting_type"),
131
+ )
132
+
133
+ participants = raw.get("participants") or []
134
+
135
+ speakers = [
136
+ Speaker(
137
+ name=s.get("name", ""),
138
+ speaker_id=s.get("speaker_id"),
139
+ duration=s.get("duration", 0),
140
+ duration_pct=s.get("duration_pct", 0.0),
141
+ word_count=s.get("word_count", 0),
142
+ words_per_minute=s.get("words_per_minute", 0.0),
143
+ filler_words=s.get("filler_words", 0),
144
+ questions=s.get("questions", 0),
145
+ longest_monologue=s.get("longest_monologue", 0),
146
+ monologues_count=s.get("monologues_count", 0),
147
+ )
148
+ for s in (analytics_raw.get("speakers") or [])
149
+ ]
150
+
151
+ action_items = [
152
+ ActionItem(text=item, transcript_id=raw.get("id"))
153
+ for item in summary.action_items_list
154
+ ]
155
+
156
+ topics = [
157
+ Topic(text=t, transcript_id=raw.get("id"))
158
+ for t in summary.topics_list
159
+ ]
160
+
161
+ sentences = [
162
+ Sentence(
163
+ index=s.get("index", 0),
164
+ speaker_name=s.get("speaker_name", ""),
165
+ speaker_id=s.get("speaker_id"),
166
+ text=s.get("text", ""),
167
+ raw_text=s.get("raw_text", ""),
168
+ start_time=s.get("start_time", 0.0),
169
+ end_time=s.get("end_time", 0.0),
170
+ )
171
+ for s in (raw.get("sentences") or [])
172
+ ]
173
+
174
+ return Transcript(
175
+ id=raw["id"],
176
+ title=raw.get("title", "Untitled"),
177
+ date=str(raw.get("date", "")),
178
+ duration=raw.get("duration", 0),
179
+ organizer_email=raw.get("organizer_email"),
180
+ participants=participants,
181
+ summary=summary,
182
+ speakers=speakers,
183
+ action_items=action_items,
184
+ topics=topics,
185
+ sentences=sentences,
186
+ raw=raw,
187
+ )
188
+
189
+
190
+ class FirefliesClient:
191
+ def __init__(self, api_key: str) -> None:
192
+ transport = HTTPXTransport(
193
+ url=FIREFLIES_API_URL,
194
+ headers={"Authorization": f"Bearer {api_key}"},
195
+ )
196
+ self._client = Client(transport=transport, fetch_schema_from_transport=False)
197
+
198
+ def _execute(self, query, variables: dict | None = None) -> dict:
199
+ try:
200
+ result = self._client.execute(query, variable_values=variables or {})
201
+ return result
202
+ except Exception as e:
203
+ msg = str(e).lower()
204
+ if "429" in str(e) or "rate limit" in msg:
205
+ raise ApiError("rate limit exceeded — retry later") from e
206
+ if "unauthenticated" in msg or "unauthorized" in msg or "401" in str(e):
207
+ raise AuthError("API key missing or expired") from e
208
+ if any(code in str(e) for code in ("500", "502", "503")):
209
+ raise ApiError(f"Fireflies API error: {e}") from e
210
+ raise ApiError(str(e)) from e
211
+
212
+ def list_transcripts(
213
+ self,
214
+ limit: int = 10,
215
+ skip: int = 0,
216
+ keyword: str | None = None,
217
+ from_date: str | None = None,
218
+ to_date: str | None = None,
219
+ participants: list[str] | None = None,
220
+ ) -> list[Transcript]:
221
+ vars: dict = {"limit": limit, "skip": skip}
222
+ if keyword:
223
+ vars["keyword"] = keyword
224
+ if from_date:
225
+ vars["fromDate"] = from_date
226
+ if to_date:
227
+ vars["toDate"] = to_date
228
+ if participants:
229
+ vars["participants"] = participants
230
+ result = self._execute(LIST_TRANSCRIPTS_QUERY, vars)
231
+ return [_parse_transcript(t) for t in (result.get("transcripts") or [])]
232
+
233
+ def get_transcript(self, transcript_id: str) -> Transcript | None:
234
+ result = self._execute(GET_TRANSCRIPT_QUERY, {"id": transcript_id})
235
+ raw = result.get("transcript")
236
+ if raw is None:
237
+ return None
238
+ return _parse_transcript(raw)
239
+
240
+ def get_transcript_with_sentences(self, transcript_id: str) -> Transcript | None:
241
+ result = self._execute(GET_TRANSCRIPT_WITH_SENTENCES_QUERY, {"id": transcript_id})
242
+ raw = result.get("transcript")
243
+ if raw is None:
244
+ return None
245
+ return _parse_transcript(raw)
246
+
247
+ def search_transcripts(
248
+ self,
249
+ query: str,
250
+ limit: int = 20,
251
+ from_date: str | None = None,
252
+ to_date: str | None = None,
253
+ participants: list[str] | None = None,
254
+ ) -> list[Transcript]:
255
+ return self.list_transcripts(
256
+ limit=limit,
257
+ keyword=query,
258
+ from_date=from_date,
259
+ to_date=to_date,
260
+ participants=participants,
261
+ )
@@ -0,0 +1,29 @@
1
+ import typer
2
+ from ffx.commands.auth import auth
3
+ from ffx.commands.list_cmd import list_cmd
4
+ from ffx.commands.get import get
5
+ from ffx.commands.search import search
6
+ from ffx.commands.summary import summary
7
+ from ffx.commands.brief import brief
8
+ from ffx.commands.action_items import action_items
9
+ from ffx.commands.export import export
10
+ from ffx.commands.topics import topics
11
+ from ffx.commands.speaker import speaker
12
+ from ffx.commands.transcript import transcript
13
+ from ffx.commands.week import week, month
14
+
15
+ app = typer.Typer(name="ffx", help="Fireflies.ai CLI", no_args_is_help=True)
16
+
17
+ app.command()(auth)
18
+ app.command("list")(list_cmd)
19
+ app.command()(get)
20
+ app.command()(search)
21
+ app.command()(summary)
22
+ app.command()(brief)
23
+ app.command("action-items")(action_items)
24
+ app.command()(export)
25
+ app.command()(transcript)
26
+ app.command()(topics)
27
+ app.command()(speaker)
28
+ app.command()(week)
29
+ app.command()(month)
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+ from datetime import datetime, timedelta, timezone
4
+
5
+ import typer
6
+ from ffx.config import Config
7
+ from ffx.api_client import FirefliesClient, AuthError, ApiError
8
+ from ffx.models import ActionItem
9
+ from ffx.output import json_envelope, json_error
10
+
11
+
12
+ def action_items(
13
+ days: Optional[int] = typer.Option(7, "--days", help="Last N days"),
14
+ owner: Optional[str] = typer.Option(None, "--filter", help="Filter action items by keyword"),
15
+ limit: int = typer.Option(20, "--limit"),
16
+ table: bool = typer.Option(False, "--table", help="Rich table output"),
17
+ ) -> None:
18
+ """List action items from recent meetings."""
19
+ json_mode = not table
20
+ config = Config()
21
+ if not config.api_key:
22
+ if json_mode:
23
+ typer.echo(json_error("API key not configured", "UNAUTHENTICATED", "Run: ffx auth"))
24
+ else:
25
+ typer.echo("API key not set. Run: ffx auth", err=True)
26
+ raise typer.Exit(2)
27
+
28
+ from_date = None
29
+ if days:
30
+ from_date = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
31
+
32
+ try:
33
+ client = FirefliesClient(config.api_key)
34
+ transcripts = client.list_transcripts(limit=limit, from_date=from_date)
35
+ except AuthError:
36
+ if json_mode:
37
+ typer.echo(json_error("API key may have expired", "UNAUTHENTICATED", "Run: ffx auth"))
38
+ else:
39
+ typer.echo("API key may have expired. Run: ffx auth", err=True)
40
+ raise typer.Exit(2)
41
+ except ApiError as e:
42
+ if json_mode:
43
+ typer.echo(json_error(str(e), "API_ERROR"))
44
+ else:
45
+ typer.echo(f"API error: {e}", err=True)
46
+ raise typer.Exit(2)
47
+
48
+ all_items: list[ActionItem] = []
49
+ for t in transcripts:
50
+ for item in t.action_items:
51
+ if owner:
52
+ text_lower = item.text.lower()
53
+ assignee_lower = (item.assignee or "").lower()
54
+ if owner.lower() not in text_lower and owner.lower() not in assignee_lower:
55
+ continue
56
+ all_items.append(item)
57
+
58
+ if not all_items:
59
+ if json_mode:
60
+ typer.echo(json_envelope([], {"days": days, "owner": owner}))
61
+ else:
62
+ typer.echo("No action items found.")
63
+ return
64
+
65
+ if json_mode:
66
+ results = [
67
+ {"text": i.text, "assignee": i.assignee, "transcript_id": i.transcript_id}
68
+ for i in all_items
69
+ ]
70
+ typer.echo(json_envelope(results, {"days": days, "owner": owner}))
71
+ else:
72
+ for i, item in enumerate(all_items, 1):
73
+ assignee = f" ({item.assignee})" if item.assignee else ""
74
+ typer.echo(f" {i}. {item.text}{assignee}")
ffx/commands/auth.py ADDED
@@ -0,0 +1,20 @@
1
+ import typer
2
+ from ffx.config import Config
3
+
4
+
5
+ def auth() -> None:
6
+ """Configure your Fireflies API key."""
7
+ config = Config()
8
+
9
+ if config.api_key:
10
+ confirm = typer.confirm("An API key is already set. Overwrite?")
11
+ if not confirm:
12
+ raise typer.Exit(0)
13
+
14
+ key = typer.prompt("Enter your Fireflies API key", hide_input=True)
15
+ if not key or not key.strip():
16
+ typer.echo("Error: API key cannot be empty.", err=True)
17
+ raise typer.Exit(1)
18
+
19
+ config.set("api_key", key.strip())
20
+ typer.echo("API key saved. Run: ffx list to verify.")
ffx/commands/brief.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+ import typer
3
+ from ffx.config import Config
4
+ from ffx.api_client import FirefliesClient, AuthError, ApiError
5
+ from ffx.output import json_error, print_summary_brief
6
+
7
+
8
+ def brief(
9
+ transcript_id: str = typer.Argument(..., help="Transcript ID"),
10
+ table: bool = typer.Option(False, "--table", help="Rich table output"),
11
+ ) -> None:
12
+ """Show a formatted AI brief for a meeting."""
13
+ json_mode = not table
14
+ config = Config()
15
+ if not config.api_key:
16
+ if json_mode:
17
+ typer.echo(json_error("API key not configured", "UNAUTHENTICATED", "Run: ffx auth"))
18
+ else:
19
+ typer.echo("API key not set. Run: ffx auth", err=True)
20
+ raise typer.Exit(2)
21
+
22
+ try:
23
+ client = FirefliesClient(config.api_key)
24
+ t = client.get_transcript(transcript_id)
25
+ except AuthError:
26
+ if json_mode:
27
+ typer.echo(json_error("API key may have expired", "UNAUTHENTICATED", "Run: ffx auth"))
28
+ else:
29
+ typer.echo("API key may have expired. Run: ffx auth", err=True)
30
+ raise typer.Exit(2)
31
+ except ApiError as e:
32
+ if json_mode:
33
+ typer.echo(json_error(str(e), "API_ERROR"))
34
+ else:
35
+ typer.echo(f"API error: {e}", err=True)
36
+ raise typer.Exit(2)
37
+
38
+ if t is None:
39
+ if json_mode:
40
+ typer.echo(json_error("Transcript not found", "NOT_FOUND"))
41
+ else:
42
+ typer.echo(f"Transcript '{transcript_id}' not found.", err=True)
43
+ raise typer.Exit(3)
44
+
45
+ print_summary_brief(t, json_mode=json_mode)
ffx/commands/export.py ADDED
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+ import csv
3
+ import io
4
+ import json
5
+ from enum import Enum
6
+
7
+ import typer
8
+ from ffx.config import Config
9
+ from ffx.api_client import FirefliesClient, AuthError, ApiError
10
+ from ffx.output import json_error
11
+
12
+
13
+ class ExportFormat(str, Enum):
14
+ json = "json"
15
+ md = "md"
16
+ obsidian = "obsidian"
17
+ csv = "csv"
18
+
19
+
20
+ def _to_md(t) -> str:
21
+ lines = [f"# {t.title}", "", f"**Date:** {t.display_date}"]
22
+ if t.organizer_email:
23
+ lines.append(f"**Organizer:** {t.organizer_email}")
24
+ if t.participants:
25
+ names = ", ".join(t.participants)
26
+ lines.append(f"**Participants:** {names}")
27
+ lines.append("")
28
+ if t.summary and t.summary.overview:
29
+ lines += ["## Overview", "", t.summary.overview, ""]
30
+ if t.summary and t.summary.bullet_gist_list:
31
+ lines += ["## Key Points", ""]
32
+ for b in t.summary.bullet_gist_list:
33
+ lines.append(f"- {b}")
34
+ lines.append("")
35
+ if t.summary and t.summary.action_items_list:
36
+ lines += ["## Action Items", ""]
37
+ for item in t.summary.action_items_list:
38
+ lines.append(f"- [ ] {item}")
39
+ lines.append("")
40
+ if t.summary and t.summary.keywords:
41
+ lines.append(f"**Keywords:** {', '.join(t.summary.keywords)}")
42
+ return "\n".join(lines)
43
+
44
+
45
+ def _yaml_str(s: str) -> str:
46
+ if any(c in s for c in (':', '#', '"', "'", '{', '[', '>', '|', '\n')):
47
+ escaped = s.replace('\\', '\\\\').replace('"', '\\"')
48
+ return f'"{escaped}"'
49
+ return s
50
+
51
+
52
+ def _to_obsidian(t) -> str:
53
+ participants = list(t.participants)
54
+ keywords = (t.summary.keywords if t.summary else []) or []
55
+ frontmatter = [
56
+ "---",
57
+ f"title: {_yaml_str(t.title)}",
58
+ f"date: {t.display_date}",
59
+ f"duration: {t.duration // 60 if t.duration else 0}m",
60
+ f"organizer: {t.organizer_email or ''}",
61
+ "participants:",
62
+ ]
63
+ for p in participants:
64
+ frontmatter.append(f" - {p}")
65
+ frontmatter.append("tags:")
66
+ for k in keywords:
67
+ frontmatter.append(f" - {_yaml_str(k)}")
68
+ frontmatter += ["source: fireflies", f"transcript_id: {t.id}", "---", ""]
69
+ md_body = _to_md(t)
70
+ # Strip the first line (# Title) since it's in frontmatter
71
+ body_lines = md_body.split("\n")
72
+ body = "\n".join(body_lines[1:]) if body_lines else ""
73
+ return "\n".join(frontmatter) + body
74
+
75
+
76
+ def _to_csv(t) -> str:
77
+ output = io.StringIO()
78
+ writer = csv.writer(output)
79
+ writer.writerow(["id", "title", "date", "duration_seconds", "organizer_email", "participants", "overview", "keywords"])
80
+ participants_str = "|".join(t.participants)
81
+ overview = t.summary.overview if t.summary else ""
82
+ keywords = "|".join(t.summary.keywords if t.summary else [])
83
+ writer.writerow([t.id, t.title, t.display_date, t.duration, t.organizer_email or "", participants_str, overview or "", keywords])
84
+ return output.getvalue()
85
+
86
+
87
+ def export(
88
+ transcript_id: str = typer.Argument(..., help="Transcript ID"),
89
+ format: ExportFormat = typer.Option(ExportFormat.json, "--format", help="Output format: json|md|obsidian|csv"),
90
+ ) -> None:
91
+ """Export a meeting transcript in various formats."""
92
+ config = Config()
93
+ if not config.api_key:
94
+ typer.echo(json_error("API key not configured", "UNAUTHENTICATED", "Run: ffx auth"))
95
+ raise typer.Exit(2)
96
+
97
+ try:
98
+ client = FirefliesClient(config.api_key)
99
+ t = client.get_transcript(transcript_id)
100
+ except AuthError:
101
+ typer.echo("API key may have expired. Run: ffx auth", err=True)
102
+ raise typer.Exit(2)
103
+ except ApiError as e:
104
+ typer.echo(f"API error: {e}", err=True)
105
+ raise typer.Exit(2)
106
+
107
+ if t is None:
108
+ typer.echo(f"Transcript '{transcript_id}' not found.", err=True)
109
+ raise typer.Exit(3)
110
+
111
+ if format == ExportFormat.json:
112
+ typer.echo(json.dumps({
113
+ "id": t.id, "title": t.title, "date": t.display_date,
114
+ "duration_seconds": t.duration, "organizer_email": t.organizer_email,
115
+ "participants": t.participants,
116
+ "summary": {
117
+ "overview": t.summary.overview,
118
+ "bullet_gist": t.summary.bullet_gist_list,
119
+ "action_items": t.summary.action_items_list,
120
+ "keywords": t.summary.keywords,
121
+ "topics_discussed": t.summary.topics_list,
122
+ } if t.summary else None,
123
+ }, indent=2))
124
+ elif format == ExportFormat.md:
125
+ typer.echo(_to_md(t))
126
+ elif format == ExportFormat.obsidian:
127
+ typer.echo(_to_obsidian(t))
128
+ elif format == ExportFormat.csv:
129
+ typer.echo(_to_csv(t), nl=False)
ffx/commands/get.py ADDED
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+ import json
3
+ import typer
4
+ from ffx.config import Config
5
+ from ffx.api_client import FirefliesClient, AuthError, ApiError
6
+ from ffx.output import json_error, transcript_detail
7
+
8
+
9
+ def get(
10
+ transcript_id: str = typer.Argument(..., help="Transcript ID"),
11
+ table: bool = typer.Option(False, "--table", help="Rich table output"),
12
+ ) -> None:
13
+ """Fetch a single transcript by ID."""
14
+ json_mode = not table
15
+ config = Config()
16
+ if not config.api_key:
17
+ if json_mode:
18
+ typer.echo(json_error("API key not configured", "UNAUTHENTICATED", "Run: ffx auth"))
19
+ else:
20
+ typer.echo("API key not set. Run: ffx auth", err=True)
21
+ raise typer.Exit(2)
22
+
23
+ try:
24
+ client = FirefliesClient(config.api_key)
25
+ t = client.get_transcript(transcript_id)
26
+ except AuthError:
27
+ if json_mode:
28
+ typer.echo(json_error("API key may have expired", "UNAUTHENTICATED", "Run: ffx auth"))
29
+ else:
30
+ typer.echo("API key may have expired. Run: ffx auth", err=True)
31
+ raise typer.Exit(2)
32
+ except ApiError as e:
33
+ if json_mode:
34
+ typer.echo(json_error(str(e), "API_ERROR"))
35
+ else:
36
+ typer.echo(f"API error: {e}", err=True)
37
+ raise typer.Exit(2)
38
+
39
+ if t is None:
40
+ if json_mode:
41
+ typer.echo(json_error(f"Transcript {transcript_id!r} not found", "NOT_FOUND"))
42
+ else:
43
+ typer.echo(f"Transcript '{transcript_id}' not found.", err=True)
44
+ raise typer.Exit(3)
45
+
46
+ if json_mode:
47
+ typer.echo(json.dumps({
48
+ "id": t.id,
49
+ "title": t.title,
50
+ "date": t.display_date,
51
+ "duration_seconds": t.duration,
52
+ "organizer_email": t.organizer_email,
53
+ "participants": t.participants,
54
+ "summary": {
55
+ "overview": t.summary.overview,
56
+ "gist": t.summary.gist,
57
+ "bullet_gist": t.summary.bullet_gist_list,
58
+ "keywords": t.summary.keywords,
59
+ "topics_discussed": t.summary.topics_list,
60
+ "action_items": t.summary.action_items_list,
61
+ } if t.summary else None,
62
+ }, indent=2))
63
+ else:
64
+ transcript_detail(t)