convoviz 0.2.5__py3-none-any.whl → 0.2.7__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.
- convoviz/analysis/graphs.py +610 -242
- convoviz/cli.py +9 -1
- convoviz/config.py +11 -0
- convoviz/io/writers.py +58 -2
- convoviz/pipeline.py +1 -0
- convoviz/renderers/markdown.py +24 -4
- {convoviz-0.2.5.dist-info → convoviz-0.2.7.dist-info}/METADATA +15 -8
- {convoviz-0.2.5.dist-info → convoviz-0.2.7.dist-info}/RECORD +10 -10
- {convoviz-0.2.5.dist-info → convoviz-0.2.7.dist-info}/WHEEL +0 -0
- {convoviz-0.2.5.dist-info → convoviz-0.2.7.dist-info}/entry_points.txt +0 -0
convoviz/cli.py
CHANGED
|
@@ -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 get_default_config
|
|
8
|
+
from convoviz.config import FolderOrganization, 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
|
+
flat: bool = typer.Option(
|
|
42
|
+
False,
|
|
43
|
+
"--flat",
|
|
44
|
+
"-f",
|
|
45
|
+
help="Put all markdown files in a single folder (disables date organization).",
|
|
46
|
+
),
|
|
41
47
|
interactive: bool | None = typer.Option(
|
|
42
48
|
None,
|
|
43
49
|
"--interactive/--no-interactive",
|
|
@@ -57,6 +63,8 @@ def run(
|
|
|
57
63
|
config.input_path = input_path
|
|
58
64
|
if output_dir:
|
|
59
65
|
config.output_folder = output_dir
|
|
66
|
+
if flat:
|
|
67
|
+
config.folder_organization = FolderOrganization.FLAT
|
|
60
68
|
|
|
61
69
|
# Determine mode: interactive if explicitly requested or no input provided
|
|
62
70
|
use_interactive = interactive if interactive is not None else (input_path is None)
|
convoviz/config.py
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
"""Configuration models using Pydantic v2."""
|
|
2
2
|
|
|
3
|
+
from enum import Enum
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Literal
|
|
5
6
|
|
|
6
7
|
from pydantic import BaseModel, Field
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
class FolderOrganization(str, Enum):
|
|
11
|
+
"""How to organize markdown output files in folders."""
|
|
12
|
+
|
|
13
|
+
FLAT = "flat" # All files in one directory (default)
|
|
14
|
+
DATE = "date" # Nested by year/month/week
|
|
15
|
+
|
|
16
|
+
|
|
9
17
|
class AuthorHeaders(BaseModel):
|
|
10
18
|
"""Headers for different message authors in markdown output."""
|
|
11
19
|
|
|
@@ -74,6 +82,8 @@ class GraphConfig(BaseModel):
|
|
|
74
82
|
figsize: tuple[int, int] = (10, 6)
|
|
75
83
|
dpi: int = 300
|
|
76
84
|
timezone: Literal["utc", "local"] = "local"
|
|
85
|
+
generate_monthly_breakdowns: bool = False
|
|
86
|
+
generate_yearly_breakdowns: bool = False
|
|
77
87
|
|
|
78
88
|
|
|
79
89
|
class ConvovizConfig(BaseModel):
|
|
@@ -81,6 +91,7 @@ class ConvovizConfig(BaseModel):
|
|
|
81
91
|
|
|
82
92
|
input_path: Path | None = None
|
|
83
93
|
output_folder: Path = Field(default_factory=lambda: Path.home() / "Documents" / "ChatGPT-Data")
|
|
94
|
+
folder_organization: FolderOrganization = FolderOrganization.DATE
|
|
84
95
|
message: MessageConfig = Field(default_factory=MessageConfig)
|
|
85
96
|
conversation: ConversationConfig = Field(default_factory=ConversationConfig)
|
|
86
97
|
wordcloud: WordCloudConfig = Field(default_factory=WordCloudConfig)
|
convoviz/io/writers.py
CHANGED
|
@@ -6,12 +6,59 @@ from pathlib import Path
|
|
|
6
6
|
from orjson import OPT_INDENT_2, dumps
|
|
7
7
|
from tqdm import tqdm
|
|
8
8
|
|
|
9
|
-
from convoviz.config import AuthorHeaders, ConversationConfig
|
|
9
|
+
from convoviz.config import AuthorHeaders, ConversationConfig, FolderOrganization
|
|
10
10
|
from convoviz.io.assets import copy_asset, resolve_asset_path
|
|
11
11
|
from convoviz.models import Conversation, ConversationCollection
|
|
12
12
|
from convoviz.renderers import render_conversation
|
|
13
13
|
from convoviz.utils import sanitize
|
|
14
14
|
|
|
15
|
+
# Month names for folder naming
|
|
16
|
+
_MONTH_NAMES = [
|
|
17
|
+
"January",
|
|
18
|
+
"February",
|
|
19
|
+
"March",
|
|
20
|
+
"April",
|
|
21
|
+
"May",
|
|
22
|
+
"June",
|
|
23
|
+
"July",
|
|
24
|
+
"August",
|
|
25
|
+
"September",
|
|
26
|
+
"October",
|
|
27
|
+
"November",
|
|
28
|
+
"December",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_date_folder_path(conversation: Conversation) -> Path:
|
|
33
|
+
"""Get the date-based folder path for a conversation.
|
|
34
|
+
|
|
35
|
+
Creates a nested structure: year/month/week
|
|
36
|
+
Example: 2024/03-March/Week-02/
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
conversation: The conversation to get the path for
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Relative path for the date-based folder structure
|
|
43
|
+
"""
|
|
44
|
+
create_time = conversation.create_time
|
|
45
|
+
|
|
46
|
+
# Year folder: "2024"
|
|
47
|
+
year = str(create_time.year)
|
|
48
|
+
|
|
49
|
+
# Month folder: "03-March"
|
|
50
|
+
month_num = create_time.month
|
|
51
|
+
month_name = _MONTH_NAMES[month_num - 1]
|
|
52
|
+
month = f"{month_num:02d}-{month_name}"
|
|
53
|
+
|
|
54
|
+
# Week folder: "Week-01" through "Week-05" (week of the month)
|
|
55
|
+
# Calculate which week of the month this date falls into
|
|
56
|
+
day = create_time.day
|
|
57
|
+
week_of_month = (day - 1) // 7 + 1
|
|
58
|
+
week = f"Week-{week_of_month:02d}"
|
|
59
|
+
|
|
60
|
+
return Path(year) / month / week
|
|
61
|
+
|
|
15
62
|
|
|
16
63
|
def save_conversation(
|
|
17
64
|
conversation: Conversation,
|
|
@@ -74,6 +121,7 @@ def save_collection(
|
|
|
74
121
|
config: ConversationConfig,
|
|
75
122
|
headers: AuthorHeaders,
|
|
76
123
|
*,
|
|
124
|
+
folder_organization: FolderOrganization = FolderOrganization.FLAT,
|
|
77
125
|
progress_bar: bool = False,
|
|
78
126
|
) -> None:
|
|
79
127
|
"""Save all conversations in a collection to markdown files.
|
|
@@ -83,6 +131,7 @@ def save_collection(
|
|
|
83
131
|
directory: Target directory
|
|
84
132
|
config: Conversation rendering configuration
|
|
85
133
|
headers: Author header configuration
|
|
134
|
+
folder_organization: How to organize files in folders (flat or by date)
|
|
86
135
|
progress_bar: Whether to show a progress bar
|
|
87
136
|
"""
|
|
88
137
|
directory.mkdir(parents=True, exist_ok=True)
|
|
@@ -92,7 +141,14 @@ def save_collection(
|
|
|
92
141
|
desc="Writing Markdown 📄 files",
|
|
93
142
|
disable=not progress_bar,
|
|
94
143
|
):
|
|
95
|
-
|
|
144
|
+
# Determine target directory based on organization setting
|
|
145
|
+
if folder_organization == FolderOrganization.DATE:
|
|
146
|
+
target_dir = directory / get_date_folder_path(conv)
|
|
147
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
else:
|
|
149
|
+
target_dir = directory
|
|
150
|
+
|
|
151
|
+
filepath = target_dir / f"{sanitize(conv.title)}.md"
|
|
96
152
|
save_conversation(conv, filepath, config, headers, source_path=collection.source_path)
|
|
97
153
|
|
|
98
154
|
|
convoviz/pipeline.py
CHANGED
convoviz/renderers/markdown.py
CHANGED
|
@@ -8,6 +8,24 @@ from convoviz.exceptions import MessageContentError
|
|
|
8
8
|
from convoviz.models import Conversation, Node
|
|
9
9
|
from convoviz.renderers.yaml import render_yaml_header
|
|
10
10
|
|
|
11
|
+
# Length for shortened node IDs in markdown output (similar to Git short hashes)
|
|
12
|
+
SHORT_ID_LENGTH = 8
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def shorten_id(node_id: str) -> str:
|
|
16
|
+
"""Shorten a node ID for display in markdown.
|
|
17
|
+
|
|
18
|
+
Takes the first 8 characters of the ID, which is typically the first
|
|
19
|
+
segment of a UUID and provides sufficient uniqueness within a conversation.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
node_id: The full node ID (often a UUID)
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Shortened ID string
|
|
26
|
+
"""
|
|
27
|
+
return node_id[:SHORT_ID_LENGTH]
|
|
28
|
+
|
|
11
29
|
|
|
12
30
|
def close_code_blocks(text: str) -> str:
|
|
13
31
|
"""Ensure all code blocks in the text are properly closed.
|
|
@@ -105,10 +123,10 @@ def render_node_header(node: Node, headers: AuthorHeaders, flavor: str = "standa
|
|
|
105
123
|
|
|
106
124
|
# Add parent link if parent has a message
|
|
107
125
|
if node.parent_node and node.parent_node.message:
|
|
108
|
-
parts.append(f"[⬆️](#^{node.parent_node.id})")
|
|
126
|
+
parts.append(f"[⬆️](#^{shorten_id(node.parent_node.id)})")
|
|
109
127
|
|
|
110
128
|
author_header = render_message_header(node.message.author.role, headers)
|
|
111
|
-
parts.append(f"{author_header} ^{node.id}")
|
|
129
|
+
parts.append(f"{author_header} ^{shorten_id(node.id)}")
|
|
112
130
|
|
|
113
131
|
return "\n".join(parts) + "\n"
|
|
114
132
|
|
|
@@ -127,9 +145,11 @@ def render_node_footer(node: Node, flavor: str = "standard") -> str:
|
|
|
127
145
|
return ""
|
|
128
146
|
|
|
129
147
|
if len(node.children_nodes) == 1:
|
|
130
|
-
return f"\n[⬇️](#^{node.children_nodes[0].id})\n"
|
|
148
|
+
return f"\n[⬇️](#^{shorten_id(node.children_nodes[0].id)})\n"
|
|
131
149
|
|
|
132
|
-
links = " | ".join(
|
|
150
|
+
links = " | ".join(
|
|
151
|
+
f"[{i + 1} ⬇️](#^{shorten_id(child.id)})" for i, child in enumerate(node.children_nodes)
|
|
152
|
+
)
|
|
133
153
|
return f"\n{links}\n"
|
|
134
154
|
|
|
135
155
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: convoviz
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
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
|
|
@@ -84,11 +84,6 @@ You can provide arguments directly to skip the prompts:
|
|
|
84
84
|
convoviz --input path/to/your/export.zip --output path/to/output/folder
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
Inputs can be any of:
|
|
88
|
-
- A ChatGPT export ZIP (downloaded from OpenAI)
|
|
89
|
-
- An extracted export directory containing `conversations.json`
|
|
90
|
-
- A `conversations.json` file directly
|
|
91
|
-
|
|
92
87
|
Notes:
|
|
93
88
|
- `--zip` / `-z` is kept as an alias for `--input` for convenience.
|
|
94
89
|
- You can force non-interactive mode with `--no-interactive`.
|
|
@@ -101,7 +96,19 @@ convoviz --help
|
|
|
101
96
|
|
|
102
97
|
### 4. Check the Output 🎉
|
|
103
98
|
|
|
104
|
-
And that's it! After running the script, head over to the output folder to see your
|
|
99
|
+
And that's it! After running the script, head over to the output folder to see your neatly formatted Markdown files and visualizations.
|
|
100
|
+
|
|
101
|
+
The main outputs are:
|
|
102
|
+
|
|
103
|
+
- **`Markdown/`**: one `.md` file per conversation
|
|
104
|
+
- **`Graphs/`**: a small set of high-signal plots, including:
|
|
105
|
+
- `overview.png` (dashboard)
|
|
106
|
+
- `activity_heatmap.png` (weekday × hour)
|
|
107
|
+
- `daily_activity.png` / `monthly_activity.png`
|
|
108
|
+
- `model_usage.png`, `conversation_lengths.png`
|
|
109
|
+
- `weekday_pattern.png`, `hourly_pattern.png`, `conversation_lifetimes.png`
|
|
110
|
+
- **`Word-Clouds/`**: weekly/monthly/yearly word clouds
|
|
111
|
+
- **`custom_instructions.json`**: extracted custom instructions
|
|
105
112
|
|
|
106
113
|
## Share Your Feedback! 💌
|
|
107
114
|
|
|
@@ -119,7 +126,7 @@ And if you've had a great experience, consider giving the project a star ⭐. It
|
|
|
119
126
|
|
|
120
127
|
## Notes
|
|
121
128
|
|
|
122
|
-
This is just a small thing I coded to help me see my convos in beautiful markdown
|
|
129
|
+
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 (and you can choose an Obsidian-flavored mode in the interactive config if you want block IDs / navigation links).
|
|
123
130
|
|
|
124
131
|
I wasn't a fan of the clunky, and sometimes paid, browser extensions.
|
|
125
132
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
convoviz/__init__.py,sha256=bQLCHO2U9EyMTGqNgsYiCtBQKTKNj4iIM3-TwIkrnRY,612
|
|
2
2
|
convoviz/__main__.py,sha256=1qiGW13_SgL7wJi8iioIN-AAHGkNGnEl5q_RcPUrI0s,143
|
|
3
3
|
convoviz/analysis/__init__.py,sha256=FxgH5JJpyypiLJpMQn_HlM51jnb8lQdP63_C_W3Dlx4,241
|
|
4
|
-
convoviz/analysis/graphs.py,sha256=
|
|
4
|
+
convoviz/analysis/graphs.py,sha256=gt056UkgGcy9vCkupQmW_HjOLy-W6j4Ekxr315BXPgA,29457
|
|
5
5
|
convoviz/analysis/wordcloud.py,sha256=ZnbA_-rcXHwXIny_xbudfJDQbIuPT7urNFfHcx6QWxQ,4673
|
|
6
6
|
convoviz/assets/colormaps.txt,sha256=59TSGz428AxY14AEvymAH2IJ2RT9Mlp7Sy0N12NEdXQ,108
|
|
7
7
|
convoviz/assets/fonts/AmaticSC-Regular.ttf,sha256=83clh7a3urnTLud0_yZofuIb6BdyC2LMI9jhE6G2LvU,142696
|
|
@@ -36,26 +36,26 @@ convoviz/assets/fonts/YsabeauSC-Regular.ttf,sha256=G4lkq34KKqZOaoomtxFz_KlwVmxg5
|
|
|
36
36
|
convoviz/assets/fonts/YsabeauSC-Thin.ttf,sha256=hZGOZNTRrxbiUPE2VDeLbtnaRwkMOBaVQbq7Fwx-34c,116932
|
|
37
37
|
convoviz/assets/fonts/Zeyada-Regular.ttf,sha256=fKhkrp9VHt_3Aw8JfkfkPeC2j3CilLWuPUudzBeawPQ,57468
|
|
38
38
|
convoviz/assets/stopwords.txt,sha256=7_ywpxsKYOj3U5CZTh9lP4GqbbkZLMabSOjKAXFk6Wc,539
|
|
39
|
-
convoviz/cli.py,sha256=
|
|
40
|
-
convoviz/config.py,sha256=
|
|
39
|
+
convoviz/cli.py,sha256=TPboT0maH_b_EjiT9cWbUSyMFz4ozoqf1_R-4AzY31g,3730
|
|
40
|
+
convoviz/config.py,sha256=5fklWzwr0aNyeEJG0NAggLhT0xI0kwgxhjyh9_zUvwM,3112
|
|
41
41
|
convoviz/exceptions.py,sha256=bQpIKls48uOQpagEJAxpXf5LF7QoagRRfbD0MjWC7Ak,1476
|
|
42
42
|
convoviz/interactive.py,sha256=VXtKgYo9tZGtsoj7zThdnbTrbjSNP5MzAZbdOs3icW4,7424
|
|
43
43
|
convoviz/io/__init__.py,sha256=y70TYypJ36_kaEA04E2wa1EDaKQVjprKItoKR6MMs4M,471
|
|
44
44
|
convoviz/io/assets.py,sha256=BykidWJG6OQAgbVfUByQ3RLTrldzpZ_NeM7HV3a5Tig,2333
|
|
45
45
|
convoviz/io/loaders.py,sha256=RuGiGzpyNcgwTxOM-m2ehhyh2mP1-k1YamK8-VynR3g,5713
|
|
46
|
-
convoviz/io/writers.py,sha256=
|
|
46
|
+
convoviz/io/writers.py,sha256=UW-JF5uq91NV62qpFVqgWTYSzzOAkLv67zDpulz2iBc,5072
|
|
47
47
|
convoviz/models/__init__.py,sha256=6gAfrk6KJT2QxdvX_v15mUdfIqEw1xKxwQlKSfyA5eI,532
|
|
48
48
|
convoviz/models/collection.py,sha256=L658yKMNC6IZrfxYxZBe-oO9COP_bzVfRznnNon7tfU,4467
|
|
49
49
|
convoviz/models/conversation.py,sha256=ssx1Z6LM9kJIx3OucQW8JVoAc8zCdxj1iOLtie2B3ak,5678
|
|
50
50
|
convoviz/models/message.py,sha256=mVnaUG6hypz92Oz3OgFAK1uuTgH3ZOJAWsFiCpLYneY,5459
|
|
51
51
|
convoviz/models/node.py,sha256=1vBAtKVscYsUBDnKAOyLxuZaK9KoVF1dFXiKXRHxUnY,1946
|
|
52
|
-
convoviz/pipeline.py,sha256=
|
|
52
|
+
convoviz/pipeline.py,sha256=IKfyy3iaNDTqox2YvwB3tnPqvL5iM0i_kMoa854glvY,5806
|
|
53
53
|
convoviz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
54
54
|
convoviz/renderers/__init__.py,sha256=IQgwD9NqtUgbS9zwyPBNZbBIZcFrbZ9C7WMAV-X3Xdg,261
|
|
55
|
-
convoviz/renderers/markdown.py,sha256=
|
|
55
|
+
convoviz/renderers/markdown.py,sha256=mpDt-xrjsPX_wt9URCDk2wicesaVv_VTWWxTHCMKiLM,7765
|
|
56
56
|
convoviz/renderers/yaml.py,sha256=XG1s4VhDdx-TiqekTkgED87RZ1lVQ7IwrbA-sZHrs7k,4056
|
|
57
57
|
convoviz/utils.py,sha256=IQEKYHhWOnYxlr4GwAHoquG0BXTlVRkORL80oUSaIeQ,3417
|
|
58
|
-
convoviz-0.2.
|
|
59
|
-
convoviz-0.2.
|
|
60
|
-
convoviz-0.2.
|
|
61
|
-
convoviz-0.2.
|
|
58
|
+
convoviz-0.2.7.dist-info/WHEEL,sha256=eycQt0QpYmJMLKpE3X9iDk8R04v2ZF0x82ogq-zP6bQ,79
|
|
59
|
+
convoviz-0.2.7.dist-info/entry_points.txt,sha256=HYsmsw5vt36yYHB05uVU48AK2WLkcwshly7m7KKuZMY,54
|
|
60
|
+
convoviz-0.2.7.dist-info/METADATA,sha256=B3v-e0XrLNCEos-nS6_Ynw3-KGPxDMpvbrFgSEFYyHw,5820
|
|
61
|
+
convoviz-0.2.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|