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 +1 -0
- ffx/__main__.py +4 -0
- ffx/api_client.py +261 -0
- ffx/commands/__init__.py +29 -0
- ffx/commands/action_items.py +74 -0
- ffx/commands/auth.py +20 -0
- ffx/commands/brief.py +45 -0
- ffx/commands/export.py +129 -0
- ffx/commands/get.py +64 -0
- ffx/commands/list_cmd.py +68 -0
- ffx/commands/search.py +62 -0
- ffx/commands/speaker.py +84 -0
- ffx/commands/summary.py +78 -0
- ffx/commands/topics.py +82 -0
- ffx/commands/transcript.py +91 -0
- ffx/commands/week.py +121 -0
- ffx/config.py +46 -0
- ffx/models.py +107 -0
- ffx/output.py +106 -0
- ffx_cli-0.1.0.dist-info/METADATA +9 -0
- ffx_cli-0.1.0.dist-info/RECORD +23 -0
- ffx_cli-0.1.0.dist-info/WHEEL +4 -0
- ffx_cli-0.1.0.dist-info/entry_points.txt +2 -0
ffx/models.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class ActionItem:
|
|
7
|
+
text: str
|
|
8
|
+
assignee: str | None = None
|
|
9
|
+
speaker: str | None = None
|
|
10
|
+
transcript_id: str | None = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Topic:
|
|
15
|
+
text: str
|
|
16
|
+
transcript_id: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Speaker:
|
|
21
|
+
name: str
|
|
22
|
+
speaker_id: str | None = None
|
|
23
|
+
duration: float = 0.0
|
|
24
|
+
duration_pct: float = 0.0
|
|
25
|
+
word_count: int = 0
|
|
26
|
+
words_per_minute: float = 0.0
|
|
27
|
+
filler_words: int = 0
|
|
28
|
+
questions: int = 0
|
|
29
|
+
longest_monologue: float = 0.0
|
|
30
|
+
monologues_count: int = 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Summary:
|
|
35
|
+
overview: str | None = None
|
|
36
|
+
short_overview: str | None = None
|
|
37
|
+
gist: str | None = None
|
|
38
|
+
short_summary: str | None = None
|
|
39
|
+
bullet_gist: str | None = None
|
|
40
|
+
shorthand_bullet: str | None = None
|
|
41
|
+
action_items: str | None = None
|
|
42
|
+
keywords: list[str] = field(default_factory=list)
|
|
43
|
+
topics_discussed: str | None = None
|
|
44
|
+
outline: str | None = None
|
|
45
|
+
meeting_type: str | None = None
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def action_items_list(self) -> list[str]:
|
|
49
|
+
return _split_text(self.action_items)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def bullet_gist_list(self) -> list[str]:
|
|
53
|
+
return _split_text(self.bullet_gist)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def topics_list(self) -> list[str]:
|
|
57
|
+
return _split_text(self.topics_discussed)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _split_text(text: str | list | None) -> list[str]:
|
|
61
|
+
if text is None:
|
|
62
|
+
return []
|
|
63
|
+
if isinstance(text, list):
|
|
64
|
+
return text
|
|
65
|
+
return [line.strip() for line in text.strip().split("\n") if line.strip()]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class Sentence:
|
|
70
|
+
index: int
|
|
71
|
+
speaker_name: str
|
|
72
|
+
speaker_id: str | None = None
|
|
73
|
+
text: str = ""
|
|
74
|
+
raw_text: str = ""
|
|
75
|
+
start_time: float = 0.0
|
|
76
|
+
end_time: float = 0.0
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def timestamp(self) -> str:
|
|
80
|
+
mins = int(self.start_time // 60)
|
|
81
|
+
secs = int(self.start_time % 60)
|
|
82
|
+
return f"{mins:02d}:{secs:02d}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class Transcript:
|
|
87
|
+
id: str
|
|
88
|
+
title: str
|
|
89
|
+
date: str
|
|
90
|
+
duration: int = 0
|
|
91
|
+
organizer_email: str | None = None
|
|
92
|
+
participants: list[str] = field(default_factory=list)
|
|
93
|
+
summary: Summary | None = None
|
|
94
|
+
speakers: list[Speaker] = field(default_factory=list)
|
|
95
|
+
action_items: list[ActionItem] = field(default_factory=list)
|
|
96
|
+
topics: list[Topic] = field(default_factory=list)
|
|
97
|
+
sentences: list[Sentence] = field(default_factory=list)
|
|
98
|
+
raw: dict[str, Any] | None = None
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def display_date(self) -> str:
|
|
102
|
+
from datetime import datetime, timezone
|
|
103
|
+
try:
|
|
104
|
+
ts = int(self.date) / 1000
|
|
105
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
|
|
106
|
+
except (ValueError, TypeError):
|
|
107
|
+
return self.date or "unknown"
|
ffx/output.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich import box
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
err_console = Console(stderr=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def json_envelope(results: list[Any], filters: dict | None = None) -> str:
|
|
16
|
+
return json.dumps({
|
|
17
|
+
"source": "fireflies",
|
|
18
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
19
|
+
"filters": filters or {},
|
|
20
|
+
"results": results,
|
|
21
|
+
}, default=str, indent=2)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def json_error(message: str, code: str, hint: str | None = None) -> str:
|
|
25
|
+
obj: dict[str, Any] = {"error": message, "code": code}
|
|
26
|
+
if hint:
|
|
27
|
+
obj["hint"] = hint
|
|
28
|
+
return json.dumps(obj, indent=2)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def print_json(data: Any) -> None:
|
|
32
|
+
if isinstance(data, str):
|
|
33
|
+
print(data)
|
|
34
|
+
else:
|
|
35
|
+
print(json.dumps(data, default=str, indent=2))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def transcripts_table(transcripts, total: int | None = None) -> None:
|
|
39
|
+
table = Table(box=box.SIMPLE, show_header=True, header_style="bold")
|
|
40
|
+
table.add_column("ID", style="dim", no_wrap=True)
|
|
41
|
+
table.add_column("Title")
|
|
42
|
+
table.add_column("Date", no_wrap=True)
|
|
43
|
+
table.add_column("Duration", no_wrap=True)
|
|
44
|
+
table.add_column("Participants", no_wrap=True)
|
|
45
|
+
|
|
46
|
+
for t in transcripts:
|
|
47
|
+
duration_str = f"{t.duration // 60}m" if t.duration else "-"
|
|
48
|
+
participant_names = ", ".join(t.participants[:3])
|
|
49
|
+
if len(t.participants) > 3:
|
|
50
|
+
participant_names += f" +{len(t.participants) - 3}"
|
|
51
|
+
table.add_row(t.id[:8] + "...", t.title, t.display_date, duration_str, participant_names)
|
|
52
|
+
|
|
53
|
+
console.print(table)
|
|
54
|
+
if total is not None:
|
|
55
|
+
console.print(f"[dim]Showing {len(transcripts)} of {total} total[/dim]")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def transcript_detail(t) -> None:
|
|
59
|
+
console.print(f"\n[bold]{t.title}[/bold]")
|
|
60
|
+
console.print(f"[dim]{t.display_date} | {t.duration // 60 if t.duration else 0}m | {t.id}[/dim]\n")
|
|
61
|
+
|
|
62
|
+
if t.participants:
|
|
63
|
+
names = ", ".join(t.participants)
|
|
64
|
+
console.print(f"[bold]Participants:[/bold] {names}\n")
|
|
65
|
+
|
|
66
|
+
if t.summary and t.summary.overview:
|
|
67
|
+
console.print(f"[bold]Overview:[/bold]\n{t.summary.overview}\n")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_summary_brief(t, json_mode: bool = False) -> None:
|
|
71
|
+
if not t.summary:
|
|
72
|
+
if json_mode:
|
|
73
|
+
print_json({"error": "No summary available", "code": "NO_SUMMARY", "id": t.id})
|
|
74
|
+
else:
|
|
75
|
+
console.print("[dim]No summary available for this meeting.[/dim]")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
if json_mode:
|
|
79
|
+
print_json({
|
|
80
|
+
"id": t.id,
|
|
81
|
+
"title": t.title,
|
|
82
|
+
"date": t.display_date,
|
|
83
|
+
"overview": t.summary.overview,
|
|
84
|
+
"gist": t.summary.gist,
|
|
85
|
+
"bullet_gist": t.summary.bullet_gist_list,
|
|
86
|
+
"action_items": t.summary.action_items_list,
|
|
87
|
+
"keywords": t.summary.keywords,
|
|
88
|
+
"topics_discussed": t.summary.topics_list,
|
|
89
|
+
})
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
console.print(f"\n[bold]{t.title}[/bold] [dim]{t.display_date}[/dim]\n")
|
|
93
|
+
if t.summary.gist:
|
|
94
|
+
console.print(f"[bold]In a nutshell:[/bold] {t.summary.gist}\n")
|
|
95
|
+
if t.summary.bullet_gist_list:
|
|
96
|
+
console.print("[bold]Key points:[/bold]")
|
|
97
|
+
for point in t.summary.bullet_gist_list:
|
|
98
|
+
console.print(f" \u2022 {point}")
|
|
99
|
+
console.print()
|
|
100
|
+
if t.summary.action_items_list:
|
|
101
|
+
console.print("[bold]Action items:[/bold]")
|
|
102
|
+
for item in t.summary.action_items_list:
|
|
103
|
+
console.print(f" \u2610 {item}")
|
|
104
|
+
console.print()
|
|
105
|
+
if t.summary.topics_list:
|
|
106
|
+
console.print(f"[bold]Topics:[/bold] {', '.join(t.summary.topics_list)}\n")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
ffx/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
ffx/__main__.py,sha256=pZBhzWXwHcGxfs059P9TI_5NTifonxnEubywDgBx1ow,67
|
|
3
|
+
ffx/api_client.py,sha256=UubpBNLXutJCn5puuIgTrFO3qB9g1q40h2kiqxxGijc,8476
|
|
4
|
+
ffx/config.py,sha256=HENUCMZG0sEkNDda77LG0UofarEMCjd6B64-mtlttTM,1105
|
|
5
|
+
ffx/models.py,sha256=AQ8UcY0YB52yDt55AGFf2CXWA_Ot19mxsi-tLjp6Ytw,2757
|
|
6
|
+
ffx/output.py,sha256=jQioCr1el6q7GWjdlo3YE4XIWQKUW9YpxwbdBD4CFyI,3647
|
|
7
|
+
ffx/commands/__init__.py,sha256=rWZbwIJS_wAadFSfx0gghs76BugijGrox4x5LcrBJCA,890
|
|
8
|
+
ffx/commands/action_items.py,sha256=pQFu07rwB3U807VNHI-rR5iiguUb8YD5dGuEmiuorOo,2729
|
|
9
|
+
ffx/commands/auth.py,sha256=u1N5rJ5RhhWUuVzNp6erCEXUJY0VT6oMygpy83VqmEo,582
|
|
10
|
+
ffx/commands/brief.py,sha256=lN55fYWGhRJBbYv3_qy9uxmzW23EeGms7MhUcuQw3fY,1567
|
|
11
|
+
ffx/commands/export.py,sha256=G40_5oyg214pRlmo7RPdjBtO53D8smXeOftLthI3Q8w,4635
|
|
12
|
+
ffx/commands/get.py,sha256=Ulw-TYJe24y_0J6fIBxziYZgSBpNtMZe8trr4q9O8r4,2255
|
|
13
|
+
ffx/commands/list_cmd.py,sha256=VOU8s39dHfMG93iHDJUXOSoQw3yogB2vW05B0RVEgzU,2433
|
|
14
|
+
ffx/commands/search.py,sha256=0zAjGRiknlQLqcgRsc5r91m1ScOhJ7jieXwivNik-30,2296
|
|
15
|
+
ffx/commands/speaker.py,sha256=OXXsOJw7twxPOZMJZEfNG0DyLSEOAqww7K_iYwmCLjw,3224
|
|
16
|
+
ffx/commands/summary.py,sha256=RBzHM7_4KvNAFxs74rEjOussJu3IoHGGOlUZCGGnmX4,2833
|
|
17
|
+
ffx/commands/topics.py,sha256=-VDNf1G_D_37qPQnqmQGyPeADd2wBWH4DaZvN9QQjC8,3123
|
|
18
|
+
ffx/commands/transcript.py,sha256=oUnfm3rQvihUsk6LloOwR-4B0QyiS67aKdUfA1hCSWc,3123
|
|
19
|
+
ffx/commands/week.py,sha256=3akcbjJpBQolDYbY5qbnEUcO0im9yzjumRnPWeAIrnk,4650
|
|
20
|
+
ffx_cli-0.1.0.dist-info/METADATA,sha256=rYlRtkzYq60DTqQl3uNhZNx3tByolYqIvpHsPYof-JQ,212
|
|
21
|
+
ffx_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
22
|
+
ffx_cli-0.1.0.dist-info/entry_points.txt,sha256=Brg96la5NxbdJD4GgloeUcTpYqeLgriGFweyyhVL8oo,41
|
|
23
|
+
ffx_cli-0.1.0.dist-info/RECORD,,
|