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/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,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: ffx-cli
3
+ Version: 0.1.0
4
+ Summary: Fireflies.ai CLI
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: gql[httpx]>=3.5
7
+ Requires-Dist: pyyaml>=6.0
8
+ Requires-Dist: rich>=13.7
9
+ Requires-Dist: typer>=0.12
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ffx = ffx.__main__:app