convoviz 0.2.14__tar.gz → 0.3.1__tar.gz
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.
- {convoviz-0.2.14 → convoviz-0.3.1}/PKG-INFO +33 -40
- {convoviz-0.2.14 → convoviz-0.3.1}/README.md +29 -35
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/__init__.py +10 -1
- convoviz-0.3.1/convoviz/analysis/__init__.py +22 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/cli.py +9 -1
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/config.py +13 -0
- convoviz-0.3.1/convoviz/interactive.py +255 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/pipeline.py +81 -72
- {convoviz-0.2.14 → convoviz-0.3.1}/pyproject.toml +9 -4
- convoviz-0.2.14/convoviz/analysis/__init__.py +0 -9
- convoviz-0.2.14/convoviz/interactive.py +0 -232
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/__main__.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/analysis/graphs.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/analysis/wordcloud.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/colormaps.txt +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/AmaticSC-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/ArchitectsDaughter-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/BebasNeue-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/Borel-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/Courgette-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/CroissantOne-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/Handjet-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/IndieFlower-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/Kalam-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/Lobster-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/MartianMono-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/MartianMono-Thin.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/Montserrat-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/Mooli-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/Pacifico-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/PlayfairDisplay-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/Raleway-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/RobotoMono-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/RobotoMono-Thin.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/RobotoSlab-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/RobotoSlab-Thin.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/Ruwudu-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/Sacramento-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/SedgwickAveDisplay-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/ShadowsIntoLight-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/TitilliumWeb-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/Yellowtail-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/YsabeauOffice-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/YsabeauSC-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/YsabeauSC-Thin.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/fonts/Zeyada-Regular.ttf +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/assets/stopwords.txt +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/exceptions.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/io/__init__.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/io/assets.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/io/loaders.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/io/writers.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/models/__init__.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/models/collection.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/models/conversation.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/models/message.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/models/node.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/py.typed +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/renderers/__init__.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/renderers/markdown.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/renderers/yaml.py +0 -0
- {convoviz-0.2.14 → convoviz-0.3.1}/convoviz/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: convoviz
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Get analytics and visualizations on your ChatGPT data!
|
|
5
5
|
Keywords: markdown,chatgpt,openai,visualization,analytics,json,export,data-analysis,obsidian
|
|
6
6
|
Author: Mohamed Cheikh Sidiya
|
|
@@ -9,19 +9,18 @@ License-Expression: MIT
|
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
-
Requires-Dist: matplotlib>=3.9.4
|
|
13
|
-
Requires-Dist: nltk>=3.9.2
|
|
14
12
|
Requires-Dist: orjson>=3.11.5
|
|
15
|
-
Requires-Dist: pillow>=11.3.0
|
|
16
13
|
Requires-Dist: pydantic>=2.12.5
|
|
17
14
|
Requires-Dist: pydantic-settings>=2.7.0
|
|
18
15
|
Requires-Dist: questionary>=2.1.1
|
|
19
16
|
Requires-Dist: rich>=14.2.0
|
|
20
17
|
Requires-Dist: tqdm>=4.67.1
|
|
21
18
|
Requires-Dist: typer>=0.21.0
|
|
22
|
-
Requires-Dist:
|
|
19
|
+
Requires-Dist: nltk>=3.9.2 ; extra == 'viz'
|
|
20
|
+
Requires-Dist: wordcloud>=1.9.5 ; extra == 'viz'
|
|
23
21
|
Requires-Python: >=3.12
|
|
24
22
|
Project-URL: Repository, https://github.com/mohamed-chs/chatgpt-history-export-to-md
|
|
23
|
+
Provides-Extra: viz
|
|
25
24
|
Description-Content-Type: text/markdown
|
|
26
25
|
|
|
27
26
|
# Convoviz 📊: Visualize your entire ChatGPT data
|
|
@@ -49,23 +48,19 @@ See examples [here](demo).
|
|
|
49
48
|
|
|
50
49
|
### 2. Install the tool 🛠
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
With uv ([astral-sh/uv](https://github.com/astral-sh/uv?tab=readme-ov-file#highlights)):
|
|
53
52
|
|
|
54
53
|
```bash
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
You can install it with uv (Recommended):
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
uv tool install convoviz
|
|
54
|
+
uv tool install convoviz[viz]
|
|
62
55
|
```
|
|
63
56
|
|
|
64
57
|
or pipx:
|
|
65
58
|
```bash
|
|
66
|
-
pipx install convoviz
|
|
59
|
+
pipx install convoviz[viz]
|
|
67
60
|
```
|
|
68
61
|
|
|
62
|
+
The `[viz]` extra includes graphs and word clouds. If you only need markdown conversion, you can skip it for a faster install (`uv tool install convoviz`).
|
|
63
|
+
|
|
69
64
|
### 3. Run the tool 🏃♂️
|
|
70
65
|
|
|
71
66
|
Simply run the command and follow the prompts:
|
|
@@ -82,9 +77,28 @@ You can provide arguments directly to skip the prompts:
|
|
|
82
77
|
convoviz --input path/to/your/export.zip --output path/to/output/folder
|
|
83
78
|
```
|
|
84
79
|
|
|
85
|
-
|
|
80
|
+
##### Selective Output Generation
|
|
81
|
+
|
|
82
|
+
By default, Convoviz generates all outputs (Markdown files, graphs, and word clouds). You can select specific outputs using the `--outputs` flag:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Generate only Markdown files (fastest)
|
|
86
|
+
convoviz --input export.zip --outputs markdown
|
|
87
|
+
|
|
88
|
+
# Generate Markdown and graphs (no word clouds)
|
|
89
|
+
convoviz --input export.zip --outputs markdown --outputs graphs
|
|
90
|
+
|
|
91
|
+
# Generate all outputs (default behavior)
|
|
92
|
+
convoviz --input export.zip --outputs markdown --outputs graphs --outputs wordclouds
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
In interactive mode, you'll be prompted to select which outputs to generate.
|
|
96
|
+
|
|
97
|
+
##### Other Notes
|
|
98
|
+
|
|
86
99
|
- `--zip` / `-z` is kept as an alias for `--input` for convenience.
|
|
87
100
|
- You can force non-interactive mode with `--no-interactive`.
|
|
101
|
+
- Use `--flat` to put all Markdown files in a single folder instead of organizing by date.
|
|
88
102
|
|
|
89
103
|
For more options, run:
|
|
90
104
|
|
|
@@ -96,31 +110,13 @@ convoviz --help
|
|
|
96
110
|
|
|
97
111
|
And that's it! After running the script, head over to the output folder to see your neatly formatted Markdown files and visualizations.
|
|
98
112
|
|
|
99
|
-
The main outputs are:
|
|
100
|
-
|
|
101
|
-
- **`Markdown/`**: one `.md` file per conversation
|
|
102
|
-
- **`Graphs/`**: a small set of high-signal plots, including:
|
|
103
|
-
- `overview.png` (dashboard)
|
|
104
|
-
- `activity_heatmap.png` (weekday × hour)
|
|
105
|
-
- `daily_activity.png` / `monthly_activity.png`
|
|
106
|
-
- `model_usage.png`, `conversation_lengths.png`
|
|
107
|
-
- `weekday_pattern.png`, `hourly_pattern.png`, `conversation_lifetimes.png`
|
|
108
|
-
- **`Word-Clouds/`**: weekly/monthly/yearly word clouds
|
|
109
|
-
- **`custom_instructions.json`**: extracted custom instructions
|
|
110
|
-
|
|
111
113
|

|
|
112
114
|
|
|
113
115
|
## Share Your Feedback! 💌
|
|
114
116
|
|
|
115
117
|
I hope you find this tool useful. I'm continuously looking to improve on this, but I need your help for that.
|
|
116
118
|
|
|
117
|
-
Whether you're a tech wizard or you're new to all this, I'd love to hear about your journey with the tool. Found a quirk? Have a suggestion? Or just want to send some good vibes? I'm all ears!
|
|
118
|
-
|
|
119
|
-
**Here's how you can share your thoughts:**
|
|
120
|
-
|
|
121
|
-
1. **GitHub Issues**: For more specific feedback or if you've stumbled upon a bug, please open an [issue](https://github.com/mohamed-chs/chatgpt-history-export-to-md/issues). This helps me track and address them effectively.
|
|
122
|
-
|
|
123
|
-
2. **GitHub Discussions**: If you just want to share your general experience, have a suggestion, or maybe a cool idea for a new feature, jump into the [discussions](https://github.com/mohamed-chs/chatgpt-history-export-to-md/discussions) page. It's a more casual space where we can chat.
|
|
119
|
+
Whether you're a tech wizard or you're new to all this, I'd love to hear about your journey with the tool. Found a quirk? Have a suggestion? Or just want to send some good vibes? I'm all ears! (see [issues](https://github.com/mohamed-chs/chatgpt-history-export-to-md/issues))
|
|
124
120
|
|
|
125
121
|
And if you've had a great experience, consider giving the project a star ⭐. It keeps me motivated and helps others discover it!
|
|
126
122
|
|
|
@@ -128,9 +124,6 @@ And if you've had a great experience, consider giving the project a star ⭐. It
|
|
|
128
124
|
|
|
129
125
|
This is just a small thing I coded to help me see my convos in beautiful markdown. It was originally built with [Obsidian](https://obsidian.md/) (my go-to note-taking app) in mind, but the default output is standard Markdown.
|
|
130
126
|
|
|
131
|
-
You can choose obsidian flavored md in the cli to get extra features like:
|
|
132
|
-
- model reasoning (`reasoning_recap`, `thoughts`) rendered as collapsible `> [!NOTE]-` callouts instead of being hidden.
|
|
133
|
-
|
|
134
127
|
I wasn't a fan of the clunky, and sometimes paid, browser extensions.
|
|
135
128
|
|
|
136
129
|
It was also a great opportunity to learn more about Python and type annotations. I had mypy, pyright, and ruff all on strict mode, 'twas fun.
|
|
@@ -139,12 +132,12 @@ It should(?) also work as library, so you can import and use the models and func
|
|
|
139
132
|
|
|
140
133
|
### Offline / reproducible runs
|
|
141
134
|
|
|
142
|
-
|
|
135
|
+
Word clouds use NLTK stopwords. If you're offline and NLTK data isn't installed yet, pre-download it:
|
|
143
136
|
|
|
144
137
|
```bash
|
|
145
|
-
|
|
138
|
+
python -c "import nltk; nltk.download('stopwords')"
|
|
146
139
|
```
|
|
147
140
|
|
|
148
141
|
### Bookmarklet
|
|
149
142
|
|
|
150
|
-
There
|
|
143
|
+
There's also a JavaScript bookmarklet flow under `js/` (experimental) for exporting additional conversation data outside the official ZIP export.
|
|
@@ -23,23 +23,19 @@ See examples [here](demo).
|
|
|
23
23
|
|
|
24
24
|
### 2. Install the tool 🛠
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
With uv ([astral-sh/uv](https://github.com/astral-sh/uv?tab=readme-ov-file#highlights)):
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
You can install it with uv (Recommended):
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
uv tool install convoviz
|
|
29
|
+
uv tool install convoviz[viz]
|
|
36
30
|
```
|
|
37
31
|
|
|
38
32
|
or pipx:
|
|
39
33
|
```bash
|
|
40
|
-
pipx install convoviz
|
|
34
|
+
pipx install convoviz[viz]
|
|
41
35
|
```
|
|
42
36
|
|
|
37
|
+
The `[viz]` extra includes graphs and word clouds. If you only need markdown conversion, you can skip it for a faster install (`uv tool install convoviz`).
|
|
38
|
+
|
|
43
39
|
### 3. Run the tool 🏃♂️
|
|
44
40
|
|
|
45
41
|
Simply run the command and follow the prompts:
|
|
@@ -56,9 +52,28 @@ You can provide arguments directly to skip the prompts:
|
|
|
56
52
|
convoviz --input path/to/your/export.zip --output path/to/output/folder
|
|
57
53
|
```
|
|
58
54
|
|
|
59
|
-
|
|
55
|
+
##### Selective Output Generation
|
|
56
|
+
|
|
57
|
+
By default, Convoviz generates all outputs (Markdown files, graphs, and word clouds). You can select specific outputs using the `--outputs` flag:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Generate only Markdown files (fastest)
|
|
61
|
+
convoviz --input export.zip --outputs markdown
|
|
62
|
+
|
|
63
|
+
# Generate Markdown and graphs (no word clouds)
|
|
64
|
+
convoviz --input export.zip --outputs markdown --outputs graphs
|
|
65
|
+
|
|
66
|
+
# Generate all outputs (default behavior)
|
|
67
|
+
convoviz --input export.zip --outputs markdown --outputs graphs --outputs wordclouds
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
In interactive mode, you'll be prompted to select which outputs to generate.
|
|
71
|
+
|
|
72
|
+
##### Other Notes
|
|
73
|
+
|
|
60
74
|
- `--zip` / `-z` is kept as an alias for `--input` for convenience.
|
|
61
75
|
- You can force non-interactive mode with `--no-interactive`.
|
|
76
|
+
- Use `--flat` to put all Markdown files in a single folder instead of organizing by date.
|
|
62
77
|
|
|
63
78
|
For more options, run:
|
|
64
79
|
|
|
@@ -70,31 +85,13 @@ convoviz --help
|
|
|
70
85
|
|
|
71
86
|
And that's it! After running the script, head over to the output folder to see your neatly formatted Markdown files and visualizations.
|
|
72
87
|
|
|
73
|
-
The main outputs are:
|
|
74
|
-
|
|
75
|
-
- **`Markdown/`**: one `.md` file per conversation
|
|
76
|
-
- **`Graphs/`**: a small set of high-signal plots, including:
|
|
77
|
-
- `overview.png` (dashboard)
|
|
78
|
-
- `activity_heatmap.png` (weekday × hour)
|
|
79
|
-
- `daily_activity.png` / `monthly_activity.png`
|
|
80
|
-
- `model_usage.png`, `conversation_lengths.png`
|
|
81
|
-
- `weekday_pattern.png`, `hourly_pattern.png`, `conversation_lifetimes.png`
|
|
82
|
-
- **`Word-Clouds/`**: weekly/monthly/yearly word clouds
|
|
83
|
-
- **`custom_instructions.json`**: extracted custom instructions
|
|
84
|
-
|
|
85
88
|

|
|
86
89
|
|
|
87
90
|
## Share Your Feedback! 💌
|
|
88
91
|
|
|
89
92
|
I hope you find this tool useful. I'm continuously looking to improve on this, but I need your help for that.
|
|
90
93
|
|
|
91
|
-
Whether you're a tech wizard or you're new to all this, I'd love to hear about your journey with the tool. Found a quirk? Have a suggestion? Or just want to send some good vibes? I'm all ears!
|
|
92
|
-
|
|
93
|
-
**Here's how you can share your thoughts:**
|
|
94
|
-
|
|
95
|
-
1. **GitHub Issues**: For more specific feedback or if you've stumbled upon a bug, please open an [issue](https://github.com/mohamed-chs/chatgpt-history-export-to-md/issues). This helps me track and address them effectively.
|
|
96
|
-
|
|
97
|
-
2. **GitHub Discussions**: If you just want to share your general experience, have a suggestion, or maybe a cool idea for a new feature, jump into the [discussions](https://github.com/mohamed-chs/chatgpt-history-export-to-md/discussions) page. It's a more casual space where we can chat.
|
|
94
|
+
Whether you're a tech wizard or you're new to all this, I'd love to hear about your journey with the tool. Found a quirk? Have a suggestion? Or just want to send some good vibes? I'm all ears! (see [issues](https://github.com/mohamed-chs/chatgpt-history-export-to-md/issues))
|
|
98
95
|
|
|
99
96
|
And if you've had a great experience, consider giving the project a star ⭐. It keeps me motivated and helps others discover it!
|
|
100
97
|
|
|
@@ -102,9 +99,6 @@ And if you've had a great experience, consider giving the project a star ⭐. It
|
|
|
102
99
|
|
|
103
100
|
This is just a small thing I coded to help me see my convos in beautiful markdown. It was originally built with [Obsidian](https://obsidian.md/) (my go-to note-taking app) in mind, but the default output is standard Markdown.
|
|
104
101
|
|
|
105
|
-
You can choose obsidian flavored md in the cli to get extra features like:
|
|
106
|
-
- model reasoning (`reasoning_recap`, `thoughts`) rendered as collapsible `> [!NOTE]-` callouts instead of being hidden.
|
|
107
|
-
|
|
108
102
|
I wasn't a fan of the clunky, and sometimes paid, browser extensions.
|
|
109
103
|
|
|
110
104
|
It was also a great opportunity to learn more about Python and type annotations. I had mypy, pyright, and ruff all on strict mode, 'twas fun.
|
|
@@ -113,12 +107,12 @@ It should(?) also work as library, so you can import and use the models and func
|
|
|
113
107
|
|
|
114
108
|
### Offline / reproducible runs
|
|
115
109
|
|
|
116
|
-
|
|
110
|
+
Word clouds use NLTK stopwords. If you're offline and NLTK data isn't installed yet, pre-download it:
|
|
117
111
|
|
|
118
112
|
```bash
|
|
119
|
-
|
|
113
|
+
python -c "import nltk; nltk.download('stopwords')"
|
|
120
114
|
```
|
|
121
115
|
|
|
122
116
|
### Bookmarklet
|
|
123
117
|
|
|
124
|
-
There
|
|
118
|
+
There's also a JavaScript bookmarklet flow under `js/` (experimental) for exporting additional conversation data outside the official ZIP export.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Convoviz - ChatGPT data visualization and export tool."""
|
|
2
2
|
|
|
3
|
-
from convoviz import
|
|
3
|
+
from convoviz import config, io, models, renderers, utils
|
|
4
4
|
from convoviz.config import ConvovizConfig, get_default_config
|
|
5
5
|
from convoviz.models import Conversation, ConversationCollection, Message, Node
|
|
6
6
|
from convoviz.pipeline import run_pipeline
|
|
@@ -23,3 +23,12 @@ __all__ = [
|
|
|
23
23
|
"get_default_config",
|
|
24
24
|
"run_pipeline",
|
|
25
25
|
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def __getattr__(name: str):
|
|
29
|
+
"""Lazy import for optional submodules like analysis."""
|
|
30
|
+
if name == "analysis":
|
|
31
|
+
from convoviz import analysis
|
|
32
|
+
|
|
33
|
+
return analysis
|
|
34
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Data analysis and visualization for convoviz.
|
|
2
|
+
|
|
3
|
+
Requires the [viz] extra: pip install convoviz[viz]
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"generate_week_barplot",
|
|
8
|
+
"generate_wordcloud",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __getattr__(name: str):
|
|
13
|
+
"""Lazy import for visualization functions requiring optional dependencies."""
|
|
14
|
+
if name == "generate_week_barplot":
|
|
15
|
+
from convoviz.analysis.graphs import generate_week_barplot
|
|
16
|
+
|
|
17
|
+
return generate_week_barplot
|
|
18
|
+
if name == "generate_wordcloud":
|
|
19
|
+
from convoviz.analysis.wordcloud import generate_wordcloud
|
|
20
|
+
|
|
21
|
+
return generate_wordcloud
|
|
22
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
import typer
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
|
|
8
|
-
from convoviz.config import FolderOrganization, get_default_config
|
|
8
|
+
from convoviz.config import FolderOrganization, OutputKind, get_default_config
|
|
9
9
|
from convoviz.exceptions import ConfigurationError, InvalidZipError
|
|
10
10
|
from convoviz.interactive import run_interactive_config
|
|
11
11
|
from convoviz.io.loaders import find_latest_zip
|
|
@@ -38,6 +38,12 @@ def run(
|
|
|
38
38
|
"-o",
|
|
39
39
|
help="Path to the output directory.",
|
|
40
40
|
),
|
|
41
|
+
outputs: list[OutputKind] | None = typer.Option(
|
|
42
|
+
None,
|
|
43
|
+
"--outputs",
|
|
44
|
+
help="Output types to generate (repeatable). Options: markdown, graphs, wordclouds. "
|
|
45
|
+
"If not specified, all outputs are generated.",
|
|
46
|
+
),
|
|
41
47
|
flat: bool = typer.Option(
|
|
42
48
|
False,
|
|
43
49
|
"--flat",
|
|
@@ -63,6 +69,8 @@ def run(
|
|
|
63
69
|
config.input_path = input_path
|
|
64
70
|
if output_dir:
|
|
65
71
|
config.output_folder = output_dir
|
|
72
|
+
if outputs:
|
|
73
|
+
config.outputs = set(outputs)
|
|
66
74
|
if flat:
|
|
67
75
|
config.folder_organization = FolderOrganization.FLAT
|
|
68
76
|
|
|
@@ -14,6 +14,18 @@ class FolderOrganization(str, Enum):
|
|
|
14
14
|
DATE = "date" # Nested by year/month (default)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
class OutputKind(str, Enum):
|
|
18
|
+
"""Types of outputs that can be generated."""
|
|
19
|
+
|
|
20
|
+
MARKDOWN = "markdown" # Conversation markdown files
|
|
21
|
+
GRAPHS = "graphs" # Usage analytics graphs
|
|
22
|
+
WORDCLOUDS = "wordclouds" # Word cloud visualizations
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Default: generate all outputs
|
|
26
|
+
ALL_OUTPUTS: frozenset[OutputKind] = frozenset(OutputKind)
|
|
27
|
+
|
|
28
|
+
|
|
17
29
|
class AuthorHeaders(BaseModel):
|
|
18
30
|
"""Headers for different message authors in markdown output."""
|
|
19
31
|
|
|
@@ -93,6 +105,7 @@ class ConvovizConfig(BaseModel):
|
|
|
93
105
|
input_path: Path | None = None
|
|
94
106
|
output_folder: Path = Field(default_factory=lambda: Path.home() / "Documents" / "ChatGPT-Data")
|
|
95
107
|
folder_organization: FolderOrganization = FolderOrganization.DATE
|
|
108
|
+
outputs: set[OutputKind] = Field(default_factory=lambda: set(ALL_OUTPUTS))
|
|
96
109
|
message: MessageConfig = Field(default_factory=MessageConfig)
|
|
97
110
|
conversation: ConversationConfig = Field(default_factory=ConversationConfig)
|
|
98
111
|
wordcloud: WordCloudConfig = Field(default_factory=WordCloudConfig)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Interactive configuration prompts using questionary."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Literal, Protocol, cast
|
|
5
|
+
|
|
6
|
+
from questionary import Choice, Style, checkbox, select
|
|
7
|
+
from questionary import path as qst_path
|
|
8
|
+
from questionary import text as qst_text
|
|
9
|
+
|
|
10
|
+
from convoviz.config import ConvovizConfig, OutputKind, get_default_config
|
|
11
|
+
from convoviz.io.loaders import find_latest_zip, validate_zip
|
|
12
|
+
from convoviz.utils import colormaps, default_font_path, font_names, font_path, validate_header
|
|
13
|
+
|
|
14
|
+
CUSTOM_STYLE = Style(
|
|
15
|
+
[
|
|
16
|
+
("qmark", "fg:#34eb9b bold"),
|
|
17
|
+
("question", "bold fg:#e0e0e0"),
|
|
18
|
+
("answer", "fg:#34ebeb bold"),
|
|
19
|
+
("pointer", "fg:#e834eb bold"),
|
|
20
|
+
("highlighted", "fg:#349ceb bold"),
|
|
21
|
+
("selected", "fg:#34ebeb"),
|
|
22
|
+
("separator", "fg:#eb3434"),
|
|
23
|
+
("instruction", "fg:#eb9434"),
|
|
24
|
+
("text", "fg:#b2eb34"),
|
|
25
|
+
("disabled", "fg:#858585 italic"),
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _QuestionaryPrompt[T](Protocol):
|
|
31
|
+
def ask(self) -> T | None: ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ask_or_cancel[T](prompt: _QuestionaryPrompt[T]) -> T:
|
|
35
|
+
"""Ask a questionary prompt; treat Ctrl+C/Ctrl+D as cancelling the run.
|
|
36
|
+
|
|
37
|
+
questionary's `.ask()` returns `None` on cancellation (Ctrl+C / Ctrl+D). We
|
|
38
|
+
convert that to `KeyboardInterrupt` so callers can abort the whole
|
|
39
|
+
interactive session with a single Ctrl+C.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
result = prompt.ask()
|
|
43
|
+
if result is None:
|
|
44
|
+
raise KeyboardInterrupt
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _validate_input_path(raw: str) -> bool | str:
|
|
49
|
+
path = Path(raw)
|
|
50
|
+
if not path.exists():
|
|
51
|
+
return "Path must exist"
|
|
52
|
+
|
|
53
|
+
if path.is_dir():
|
|
54
|
+
if (path / "conversations.json").exists():
|
|
55
|
+
return True
|
|
56
|
+
return "Directory must contain conversations.json"
|
|
57
|
+
|
|
58
|
+
if path.suffix.lower() == ".json":
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
if path.suffix.lower() == ".zip":
|
|
62
|
+
return True if validate_zip(path) else "ZIP must contain conversations.json"
|
|
63
|
+
|
|
64
|
+
return "Input must be a .zip, a .json, or a directory containing conversations.json"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run_interactive_config(initial_config: ConvovizConfig | None = None) -> ConvovizConfig:
|
|
68
|
+
"""Run interactive prompts to configure convoviz.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
initial_config: Optional starting configuration (uses defaults if None)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Updated configuration based on user input
|
|
75
|
+
"""
|
|
76
|
+
config = initial_config or get_default_config()
|
|
77
|
+
|
|
78
|
+
# Set sensible defaults if not already set
|
|
79
|
+
if not config.input_path:
|
|
80
|
+
latest = find_latest_zip()
|
|
81
|
+
if latest:
|
|
82
|
+
config.input_path = latest
|
|
83
|
+
|
|
84
|
+
if not config.wordcloud.font_path:
|
|
85
|
+
config.wordcloud.font_path = default_font_path()
|
|
86
|
+
|
|
87
|
+
# Prompt for input path
|
|
88
|
+
input_default = str(config.input_path) if config.input_path else ""
|
|
89
|
+
input_result: str = _ask_or_cancel(
|
|
90
|
+
qst_path(
|
|
91
|
+
"Enter the path to the export ZIP, conversations JSON, or extracted directory:",
|
|
92
|
+
default=input_default,
|
|
93
|
+
validate=_validate_input_path,
|
|
94
|
+
style=CUSTOM_STYLE,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if input_result:
|
|
99
|
+
config.input_path = Path(input_result)
|
|
100
|
+
|
|
101
|
+
# Prompt for output folder
|
|
102
|
+
output_result: str = _ask_or_cancel(
|
|
103
|
+
qst_path(
|
|
104
|
+
"Enter the path to the output folder:",
|
|
105
|
+
default=str(config.output_folder),
|
|
106
|
+
style=CUSTOM_STYLE,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if output_result:
|
|
111
|
+
config.output_folder = Path(output_result)
|
|
112
|
+
|
|
113
|
+
# Prompt for outputs to generate
|
|
114
|
+
output_choices = [
|
|
115
|
+
Choice(title="Markdown conversations", value=OutputKind.MARKDOWN, checked=True),
|
|
116
|
+
Choice(title="Graphs (usage analytics)", value=OutputKind.GRAPHS, checked=True),
|
|
117
|
+
Choice(title="Word clouds", value=OutputKind.WORDCLOUDS, checked=True),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
selected_outputs: list[OutputKind] = _ask_or_cancel(
|
|
121
|
+
checkbox(
|
|
122
|
+
"Select outputs to generate:",
|
|
123
|
+
choices=output_choices,
|
|
124
|
+
style=CUSTOM_STYLE,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
config.outputs = set(selected_outputs) if selected_outputs else set()
|
|
129
|
+
|
|
130
|
+
# Prompt for markdown settings (only if markdown output is selected)
|
|
131
|
+
if OutputKind.MARKDOWN in config.outputs:
|
|
132
|
+
# Prompt for author headers
|
|
133
|
+
headers = config.message.author_headers
|
|
134
|
+
for role in ["user", "assistant"]:
|
|
135
|
+
current = getattr(headers, role)
|
|
136
|
+
result: str = _ask_or_cancel(
|
|
137
|
+
qst_text(
|
|
138
|
+
f"Enter the message header for '{role}':",
|
|
139
|
+
default=current,
|
|
140
|
+
validate=lambda t: validate_header(t)
|
|
141
|
+
or "Must be a valid markdown header (e.g., # Title)",
|
|
142
|
+
style=CUSTOM_STYLE,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
if result:
|
|
146
|
+
setattr(headers, role, result)
|
|
147
|
+
|
|
148
|
+
# Prompt for markdown flavor
|
|
149
|
+
flavor_result = cast(
|
|
150
|
+
Literal["standard", "obsidian"],
|
|
151
|
+
_ask_or_cancel(
|
|
152
|
+
select(
|
|
153
|
+
"Select the markdown flavor:",
|
|
154
|
+
choices=["standard", "obsidian"],
|
|
155
|
+
default=config.conversation.markdown.flavor,
|
|
156
|
+
style=CUSTOM_STYLE,
|
|
157
|
+
)
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if flavor_result:
|
|
162
|
+
config.conversation.markdown.flavor = flavor_result
|
|
163
|
+
|
|
164
|
+
# Prompt for YAML headers
|
|
165
|
+
yaml_config = config.conversation.yaml
|
|
166
|
+
yaml_choices = [
|
|
167
|
+
Choice(title=field, checked=getattr(yaml_config, field))
|
|
168
|
+
for field in [
|
|
169
|
+
"title",
|
|
170
|
+
"tags",
|
|
171
|
+
"chat_link",
|
|
172
|
+
"create_time",
|
|
173
|
+
"update_time",
|
|
174
|
+
"model",
|
|
175
|
+
"used_plugins",
|
|
176
|
+
"message_count",
|
|
177
|
+
"content_types",
|
|
178
|
+
"custom_instructions",
|
|
179
|
+
]
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
selected: list[str] = _ask_or_cancel(
|
|
183
|
+
checkbox(
|
|
184
|
+
"Select YAML metadata headers to include:",
|
|
185
|
+
choices=yaml_choices,
|
|
186
|
+
style=CUSTOM_STYLE,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
selected_set = set(selected)
|
|
191
|
+
for field_name in [
|
|
192
|
+
"title",
|
|
193
|
+
"tags",
|
|
194
|
+
"chat_link",
|
|
195
|
+
"create_time",
|
|
196
|
+
"update_time",
|
|
197
|
+
"model",
|
|
198
|
+
"used_plugins",
|
|
199
|
+
"message_count",
|
|
200
|
+
"content_types",
|
|
201
|
+
"custom_instructions",
|
|
202
|
+
]:
|
|
203
|
+
setattr(yaml_config, field_name, field_name in selected_set)
|
|
204
|
+
|
|
205
|
+
# Prompt for wordcloud settings (only if wordclouds output is selected)
|
|
206
|
+
if OutputKind.WORDCLOUDS in config.outputs:
|
|
207
|
+
# Prompt for font
|
|
208
|
+
available_fonts = font_names()
|
|
209
|
+
if available_fonts:
|
|
210
|
+
current_font = (
|
|
211
|
+
config.wordcloud.font_path.stem
|
|
212
|
+
if config.wordcloud.font_path
|
|
213
|
+
else available_fonts[0]
|
|
214
|
+
)
|
|
215
|
+
font_result: str = _ask_or_cancel(
|
|
216
|
+
select(
|
|
217
|
+
"Select the font for word clouds:",
|
|
218
|
+
choices=available_fonts,
|
|
219
|
+
default=current_font if current_font in available_fonts else available_fonts[0],
|
|
220
|
+
style=CUSTOM_STYLE,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if font_result:
|
|
225
|
+
config.wordcloud.font_path = font_path(font_result)
|
|
226
|
+
|
|
227
|
+
# Prompt for colormap
|
|
228
|
+
available_colormaps = colormaps()
|
|
229
|
+
if available_colormaps:
|
|
230
|
+
colormap_result: str = _ask_or_cancel(
|
|
231
|
+
select(
|
|
232
|
+
"Select the color theme for word clouds:",
|
|
233
|
+
choices=available_colormaps,
|
|
234
|
+
default=config.wordcloud.colormap
|
|
235
|
+
if config.wordcloud.colormap in available_colormaps
|
|
236
|
+
else available_colormaps[0],
|
|
237
|
+
style=CUSTOM_STYLE,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if colormap_result:
|
|
242
|
+
config.wordcloud.colormap = colormap_result
|
|
243
|
+
|
|
244
|
+
# Prompt for custom stopwords
|
|
245
|
+
stopwords_result: str = _ask_or_cancel(
|
|
246
|
+
qst_text(
|
|
247
|
+
"Enter custom stopwords (comma-separated):",
|
|
248
|
+
default=config.wordcloud.custom_stopwords,
|
|
249
|
+
style=CUSTOM_STYLE,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
config.wordcloud.custom_stopwords = stopwords_result
|
|
254
|
+
|
|
255
|
+
return config
|
|
@@ -5,16 +5,14 @@ from shutil import rmtree
|
|
|
5
5
|
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
|
|
8
|
-
from convoviz.
|
|
9
|
-
from convoviz.
|
|
10
|
-
from convoviz.config import ConvovizConfig
|
|
11
|
-
from convoviz.exceptions import InvalidZipError
|
|
8
|
+
from convoviz.config import ConvovizConfig, OutputKind
|
|
9
|
+
from convoviz.exceptions import ConfigurationError, InvalidZipError
|
|
12
10
|
from convoviz.io.loaders import (
|
|
13
11
|
find_latest_bookmarklet_json,
|
|
14
12
|
load_collection_from_json,
|
|
15
13
|
load_collection_from_zip,
|
|
16
14
|
)
|
|
17
|
-
from convoviz.io.writers import save_collection
|
|
15
|
+
from convoviz.io.writers import save_collection
|
|
18
16
|
|
|
19
17
|
console = Console()
|
|
20
18
|
|
|
@@ -80,10 +78,21 @@ def run_pipeline(config: ConvovizConfig) -> None:
|
|
|
80
78
|
output_folder = config.output_folder
|
|
81
79
|
output_folder.mkdir(parents=True, exist_ok=True)
|
|
82
80
|
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
# Determine which outputs are selected
|
|
82
|
+
selected_outputs = config.outputs
|
|
83
|
+
|
|
84
|
+
# Build mapping of output kind -> directory name
|
|
85
|
+
output_dir_map: dict[OutputKind, str] = {
|
|
86
|
+
OutputKind.MARKDOWN: "Markdown",
|
|
87
|
+
OutputKind.GRAPHS: "Graphs",
|
|
88
|
+
OutputKind.WORDCLOUDS: "Word-Clouds",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Clean only specific sub-directories we manage (only for selected outputs)
|
|
92
|
+
for output_kind, dir_name in output_dir_map.items():
|
|
93
|
+
if output_kind not in selected_outputs:
|
|
94
|
+
continue
|
|
95
|
+
sub_dir = output_folder / dir_name
|
|
87
96
|
if sub_dir.exists():
|
|
88
97
|
# Never follow symlinks; just unlink them.
|
|
89
98
|
if sub_dir.is_symlink():
|
|
@@ -94,69 +103,69 @@ def run_pipeline(config: ConvovizConfig) -> None:
|
|
|
94
103
|
sub_dir.unlink()
|
|
95
104
|
sub_dir.mkdir(exist_ok=True)
|
|
96
105
|
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
106
|
+
# Save markdown files (if selected)
|
|
107
|
+
if OutputKind.MARKDOWN in selected_outputs:
|
|
108
|
+
markdown_folder = output_folder / "Markdown"
|
|
109
|
+
save_collection(
|
|
110
|
+
collection,
|
|
111
|
+
markdown_folder,
|
|
112
|
+
config.conversation,
|
|
113
|
+
config.message.author_headers,
|
|
114
|
+
folder_organization=config.folder_organization,
|
|
115
|
+
progress_bar=True,
|
|
116
|
+
)
|
|
117
|
+
console.print(
|
|
118
|
+
f"\nDone [bold green]✅[/bold green] ! "
|
|
119
|
+
f"Check the output [bold blue]📄[/bold blue] here: {_safe_uri(markdown_folder)} 🔗\n"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Generate graphs (if selected)
|
|
123
|
+
if OutputKind.GRAPHS in selected_outputs:
|
|
124
|
+
# Lazy import to allow markdown-only usage without matplotlib
|
|
125
|
+
try:
|
|
126
|
+
from convoviz.analysis.graphs import generate_graphs
|
|
127
|
+
except ModuleNotFoundError as e:
|
|
128
|
+
raise ConfigurationError(
|
|
129
|
+
"Graph generation requires matplotlib. "
|
|
130
|
+
"Install with: pip install convoviz[viz]"
|
|
131
|
+
) from e
|
|
132
|
+
|
|
133
|
+
graph_folder = output_folder / "Graphs"
|
|
134
|
+
graph_folder.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
generate_graphs(
|
|
136
|
+
collection,
|
|
137
|
+
graph_folder,
|
|
138
|
+
config.graph,
|
|
139
|
+
progress_bar=True,
|
|
140
|
+
)
|
|
141
|
+
console.print(
|
|
142
|
+
f"\nDone [bold green]✅[/bold green] ! "
|
|
143
|
+
f"Check the output [bold blue]📈[/bold blue] here: {_safe_uri(graph_folder)} 🔗\n"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Generate word clouds (if selected)
|
|
147
|
+
if OutputKind.WORDCLOUDS in selected_outputs:
|
|
148
|
+
# Lazy import to allow markdown-only usage without wordcloud/nltk
|
|
149
|
+
try:
|
|
150
|
+
from convoviz.analysis.wordcloud import generate_wordclouds
|
|
151
|
+
except ModuleNotFoundError as e:
|
|
152
|
+
raise ConfigurationError(
|
|
153
|
+
"Word cloud generation requires wordcloud and nltk. "
|
|
154
|
+
"Install with: pip install convoviz[viz]"
|
|
155
|
+
) from e
|
|
156
|
+
|
|
157
|
+
wordcloud_folder = output_folder / "Word-Clouds"
|
|
158
|
+
wordcloud_folder.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
generate_wordclouds(
|
|
160
|
+
collection,
|
|
161
|
+
wordcloud_folder,
|
|
162
|
+
config.wordcloud,
|
|
163
|
+
progress_bar=True,
|
|
164
|
+
)
|
|
165
|
+
console.print(
|
|
166
|
+
f"\nDone [bold green]✅[/bold green] ! "
|
|
167
|
+
f"Check the output [bold blue]🔡☁️[/bold blue] here: {_safe_uri(wordcloud_folder)} 🔗\n"
|
|
168
|
+
)
|
|
160
169
|
|
|
161
170
|
console.print(
|
|
162
171
|
"ALL DONE [bold green]🎉🎉🎉[/bold green] !\n\n"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "convoviz"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "Get analytics and visualizations on your ChatGPT data!"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
keywords = [
|
|
@@ -18,16 +18,18 @@ authors = [
|
|
|
18
18
|
{ name = "Mohamed Cheikh Sidiya", email = "mohamedcheikhsidiya77@gmail.com" }
|
|
19
19
|
]
|
|
20
20
|
dependencies = [
|
|
21
|
-
"matplotlib>=3.9.4",
|
|
22
|
-
"nltk>=3.9.2",
|
|
23
21
|
"orjson>=3.11.5",
|
|
24
|
-
"pillow>=11.3.0",
|
|
25
22
|
"pydantic>=2.12.5",
|
|
26
23
|
"pydantic-settings>=2.7.0",
|
|
27
24
|
"questionary>=2.1.1",
|
|
28
25
|
"rich>=14.2.0",
|
|
29
26
|
"tqdm>=4.67.1",
|
|
30
27
|
"typer>=0.21.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
viz = [
|
|
32
|
+
"nltk>=3.9.2",
|
|
31
33
|
"wordcloud>=1.9.5",
|
|
32
34
|
]
|
|
33
35
|
|
|
@@ -66,6 +68,9 @@ dev = [
|
|
|
66
68
|
"ruff>=0.14.10",
|
|
67
69
|
"ty>=0.0.11",
|
|
68
70
|
"types-tqdm>=4.67.0.20250809",
|
|
71
|
+
# Viz extras for testing (wordcloud pulls in matplotlib + pillow)
|
|
72
|
+
"nltk>=3.9.2",
|
|
73
|
+
"wordcloud>=1.9.5",
|
|
69
74
|
]
|
|
70
75
|
|
|
71
76
|
[tool.ruff]
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
"""Interactive configuration prompts using questionary."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Literal, Protocol, cast
|
|
5
|
-
|
|
6
|
-
from questionary import Choice, Style, checkbox, select
|
|
7
|
-
from questionary import path as qst_path
|
|
8
|
-
from questionary import text as qst_text
|
|
9
|
-
|
|
10
|
-
from convoviz.config import ConvovizConfig, get_default_config
|
|
11
|
-
from convoviz.io.loaders import find_latest_zip, validate_zip
|
|
12
|
-
from convoviz.utils import colormaps, default_font_path, font_names, font_path, validate_header
|
|
13
|
-
|
|
14
|
-
CUSTOM_STYLE = Style(
|
|
15
|
-
[
|
|
16
|
-
("qmark", "fg:#34eb9b bold"),
|
|
17
|
-
("question", "bold fg:#e0e0e0"),
|
|
18
|
-
("answer", "fg:#34ebeb bold"),
|
|
19
|
-
("pointer", "fg:#e834eb bold"),
|
|
20
|
-
("highlighted", "fg:#349ceb bold"),
|
|
21
|
-
("selected", "fg:#34ebeb"),
|
|
22
|
-
("separator", "fg:#eb3434"),
|
|
23
|
-
("instruction", "fg:#eb9434"),
|
|
24
|
-
("text", "fg:#b2eb34"),
|
|
25
|
-
("disabled", "fg:#858585 italic"),
|
|
26
|
-
]
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class _QuestionaryPrompt[T](Protocol):
|
|
31
|
-
def ask(self) -> T | None: ...
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _ask_or_cancel[T](prompt: _QuestionaryPrompt[T]) -> T:
|
|
35
|
-
"""Ask a questionary prompt; treat Ctrl+C/Ctrl+D as cancelling the run.
|
|
36
|
-
|
|
37
|
-
questionary's `.ask()` returns `None` on cancellation (Ctrl+C / Ctrl+D). We
|
|
38
|
-
convert that to `KeyboardInterrupt` so callers can abort the whole
|
|
39
|
-
interactive session with a single Ctrl+C.
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
result = prompt.ask()
|
|
43
|
-
if result is None:
|
|
44
|
-
raise KeyboardInterrupt
|
|
45
|
-
return result
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _validate_input_path(raw: str) -> bool | str:
|
|
49
|
-
path = Path(raw)
|
|
50
|
-
if not path.exists():
|
|
51
|
-
return "Path must exist"
|
|
52
|
-
|
|
53
|
-
if path.is_dir():
|
|
54
|
-
if (path / "conversations.json").exists():
|
|
55
|
-
return True
|
|
56
|
-
return "Directory must contain conversations.json"
|
|
57
|
-
|
|
58
|
-
if path.suffix.lower() == ".json":
|
|
59
|
-
return True
|
|
60
|
-
|
|
61
|
-
if path.suffix.lower() == ".zip":
|
|
62
|
-
return True if validate_zip(path) else "ZIP must contain conversations.json"
|
|
63
|
-
|
|
64
|
-
return "Input must be a .zip, a .json, or a directory containing conversations.json"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def run_interactive_config(initial_config: ConvovizConfig | None = None) -> ConvovizConfig:
|
|
68
|
-
"""Run interactive prompts to configure convoviz.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
initial_config: Optional starting configuration (uses defaults if None)
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
Updated configuration based on user input
|
|
75
|
-
"""
|
|
76
|
-
config = initial_config or get_default_config()
|
|
77
|
-
|
|
78
|
-
# Set sensible defaults if not already set
|
|
79
|
-
if not config.input_path:
|
|
80
|
-
latest = find_latest_zip()
|
|
81
|
-
if latest:
|
|
82
|
-
config.input_path = latest
|
|
83
|
-
|
|
84
|
-
if not config.wordcloud.font_path:
|
|
85
|
-
config.wordcloud.font_path = default_font_path()
|
|
86
|
-
|
|
87
|
-
# Prompt for input path
|
|
88
|
-
input_default = str(config.input_path) if config.input_path else ""
|
|
89
|
-
input_result: str = _ask_or_cancel(
|
|
90
|
-
qst_path(
|
|
91
|
-
"Enter the path to the export ZIP, conversations JSON, or extracted directory:",
|
|
92
|
-
default=input_default,
|
|
93
|
-
validate=_validate_input_path,
|
|
94
|
-
style=CUSTOM_STYLE,
|
|
95
|
-
)
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
if input_result:
|
|
99
|
-
config.input_path = Path(input_result)
|
|
100
|
-
|
|
101
|
-
# Prompt for output folder
|
|
102
|
-
output_result: str = _ask_or_cancel(
|
|
103
|
-
qst_path(
|
|
104
|
-
"Enter the path to the output folder:",
|
|
105
|
-
default=str(config.output_folder),
|
|
106
|
-
style=CUSTOM_STYLE,
|
|
107
|
-
)
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
if output_result:
|
|
111
|
-
config.output_folder = Path(output_result)
|
|
112
|
-
|
|
113
|
-
# Prompt for author headers
|
|
114
|
-
headers = config.message.author_headers
|
|
115
|
-
for role in ["user", "assistant"]:
|
|
116
|
-
current = getattr(headers, role)
|
|
117
|
-
result: str = _ask_or_cancel(
|
|
118
|
-
qst_text(
|
|
119
|
-
f"Enter the message header for '{role}':",
|
|
120
|
-
default=current,
|
|
121
|
-
validate=lambda t: validate_header(t)
|
|
122
|
-
or "Must be a valid markdown header (e.g., # Title)",
|
|
123
|
-
style=CUSTOM_STYLE,
|
|
124
|
-
)
|
|
125
|
-
)
|
|
126
|
-
if result:
|
|
127
|
-
setattr(headers, role, result)
|
|
128
|
-
|
|
129
|
-
# Prompt for markdown flavor
|
|
130
|
-
flavor_result = cast(
|
|
131
|
-
Literal["standard", "obsidian"],
|
|
132
|
-
_ask_or_cancel(
|
|
133
|
-
select(
|
|
134
|
-
"Select the markdown flavor:",
|
|
135
|
-
choices=["standard", "obsidian"],
|
|
136
|
-
default=config.conversation.markdown.flavor,
|
|
137
|
-
style=CUSTOM_STYLE,
|
|
138
|
-
)
|
|
139
|
-
),
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
if flavor_result:
|
|
143
|
-
config.conversation.markdown.flavor = flavor_result
|
|
144
|
-
|
|
145
|
-
# Prompt for YAML headers
|
|
146
|
-
yaml_config = config.conversation.yaml
|
|
147
|
-
yaml_choices = [
|
|
148
|
-
Choice(title=field, checked=getattr(yaml_config, field))
|
|
149
|
-
for field in [
|
|
150
|
-
"title",
|
|
151
|
-
"tags",
|
|
152
|
-
"chat_link",
|
|
153
|
-
"create_time",
|
|
154
|
-
"update_time",
|
|
155
|
-
"model",
|
|
156
|
-
"used_plugins",
|
|
157
|
-
"message_count",
|
|
158
|
-
"content_types",
|
|
159
|
-
"custom_instructions",
|
|
160
|
-
]
|
|
161
|
-
]
|
|
162
|
-
|
|
163
|
-
selected: list[str] = _ask_or_cancel(
|
|
164
|
-
checkbox(
|
|
165
|
-
"Select YAML metadata headers to include:",
|
|
166
|
-
choices=yaml_choices,
|
|
167
|
-
style=CUSTOM_STYLE,
|
|
168
|
-
)
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
selected_set = set(selected)
|
|
172
|
-
for field_name in [
|
|
173
|
-
"title",
|
|
174
|
-
"tags",
|
|
175
|
-
"chat_link",
|
|
176
|
-
"create_time",
|
|
177
|
-
"update_time",
|
|
178
|
-
"model",
|
|
179
|
-
"used_plugins",
|
|
180
|
-
"message_count",
|
|
181
|
-
"content_types",
|
|
182
|
-
"custom_instructions",
|
|
183
|
-
]:
|
|
184
|
-
setattr(yaml_config, field_name, field_name in selected_set)
|
|
185
|
-
|
|
186
|
-
# Prompt for font
|
|
187
|
-
available_fonts = font_names()
|
|
188
|
-
if available_fonts:
|
|
189
|
-
current_font = (
|
|
190
|
-
config.wordcloud.font_path.stem if config.wordcloud.font_path else available_fonts[0]
|
|
191
|
-
)
|
|
192
|
-
font_result: str = _ask_or_cancel(
|
|
193
|
-
select(
|
|
194
|
-
"Select the font for word clouds:",
|
|
195
|
-
choices=available_fonts,
|
|
196
|
-
default=current_font if current_font in available_fonts else available_fonts[0],
|
|
197
|
-
style=CUSTOM_STYLE,
|
|
198
|
-
)
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
if font_result:
|
|
202
|
-
config.wordcloud.font_path = font_path(font_result)
|
|
203
|
-
|
|
204
|
-
# Prompt for colormap
|
|
205
|
-
available_colormaps = colormaps()
|
|
206
|
-
if available_colormaps:
|
|
207
|
-
colormap_result: str = _ask_or_cancel(
|
|
208
|
-
select(
|
|
209
|
-
"Select the color theme for word clouds:",
|
|
210
|
-
choices=available_colormaps,
|
|
211
|
-
default=config.wordcloud.colormap
|
|
212
|
-
if config.wordcloud.colormap in available_colormaps
|
|
213
|
-
else available_colormaps[0],
|
|
214
|
-
style=CUSTOM_STYLE,
|
|
215
|
-
)
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
if colormap_result:
|
|
219
|
-
config.wordcloud.colormap = colormap_result
|
|
220
|
-
|
|
221
|
-
# Prompt for custom stopwords
|
|
222
|
-
stopwords_result: str = _ask_or_cancel(
|
|
223
|
-
qst_text(
|
|
224
|
-
"Enter custom stopwords (comma-separated):",
|
|
225
|
-
default=config.wordcloud.custom_stopwords,
|
|
226
|
-
style=CUSTOM_STYLE,
|
|
227
|
-
)
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
config.wordcloud.custom_stopwords = stopwords_result
|
|
231
|
-
|
|
232
|
-
return config
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|