convoviz 0.4.1__py3-none-any.whl → 0.4.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/cli.py +16 -0
- convoviz/config.py +3 -0
- convoviz/interactive.py +17 -30
- convoviz/models/conversation.py +18 -0
- convoviz/models/message.py +81 -5
- convoviz/renderers/markdown.py +96 -3
- convoviz/renderers/yaml.py +4 -0
- {convoviz-0.4.1.dist-info → convoviz-0.4.7.dist-info}/METADATA +47 -29
- {convoviz-0.4.1.dist-info → convoviz-0.4.7.dist-info}/RECORD +11 -11
- {convoviz-0.4.1.dist-info → convoviz-0.4.7.dist-info}/WHEEL +0 -0
- {convoviz-0.4.1.dist-info → convoviz-0.4.7.dist-info}/entry_points.txt +0 -0
convoviz/cli.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Command-line interface for convoviz."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
from importlib.metadata import version as get_version
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
import typer
|
|
@@ -22,6 +23,13 @@ app = typer.Typer(
|
|
|
22
23
|
console = Console()
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
def _version_callback(value: bool) -> None:
|
|
27
|
+
"""Print version and exit."""
|
|
28
|
+
if value:
|
|
29
|
+
console.print(f"convoviz {get_version('convoviz')}")
|
|
30
|
+
raise typer.Exit()
|
|
31
|
+
|
|
32
|
+
|
|
25
33
|
@app.callback(invoke_without_command=True)
|
|
26
34
|
def run(
|
|
27
35
|
ctx: typer.Context,
|
|
@@ -71,6 +79,14 @@ def run(
|
|
|
71
79
|
"--log-file",
|
|
72
80
|
help="Path to log file. Defaults to a temporary file.",
|
|
73
81
|
),
|
|
82
|
+
_version: bool = typer.Option(
|
|
83
|
+
False,
|
|
84
|
+
"--version",
|
|
85
|
+
"-V",
|
|
86
|
+
help="Show version and exit.",
|
|
87
|
+
callback=_version_callback,
|
|
88
|
+
is_eager=True,
|
|
89
|
+
),
|
|
74
90
|
) -> None:
|
|
75
91
|
"""Convert ChatGPT export data to markdown and generate visualizations."""
|
|
76
92
|
# Setup logging immediately
|
convoviz/config.py
CHANGED
|
@@ -54,7 +54,10 @@ class YAMLConfig(BaseModel):
|
|
|
54
54
|
used_plugins: bool = False
|
|
55
55
|
message_count: bool = True
|
|
56
56
|
content_types: bool = False
|
|
57
|
+
content_types: bool = False
|
|
57
58
|
custom_instructions: bool = False
|
|
59
|
+
is_starred: bool = False
|
|
60
|
+
voice: bool = False
|
|
58
61
|
|
|
59
62
|
|
|
60
63
|
class ConversationConfig(BaseModel):
|
convoviz/interactive.py
CHANGED
|
@@ -8,10 +8,16 @@ from questionary import Choice, Style, checkbox, select
|
|
|
8
8
|
from questionary import path as qst_path
|
|
9
9
|
from questionary import text as qst_text
|
|
10
10
|
|
|
11
|
-
from convoviz.config import ConvovizConfig, OutputKind, get_default_config
|
|
11
|
+
from convoviz.config import ConvovizConfig, OutputKind, YAMLConfig, get_default_config
|
|
12
12
|
from convoviz.io.loaders import find_latest_zip, validate_zip
|
|
13
13
|
from convoviz.utils import colormaps, default_font_path, font_names, font_path, validate_header
|
|
14
14
|
|
|
15
|
+
OUTPUT_TITLES = {
|
|
16
|
+
OutputKind.MARKDOWN: "Markdown conversations",
|
|
17
|
+
OutputKind.GRAPHS: "Graphs (usage analytics)",
|
|
18
|
+
OutputKind.WORDCLOUDS: "Word clouds",
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
CUSTOM_STYLE = Style(
|
|
16
22
|
[
|
|
17
23
|
("qmark", "fg:#34eb9b bold"),
|
|
@@ -92,7 +98,7 @@ def run_interactive_config(initial_config: ConvovizConfig | None = None) -> Conv
|
|
|
92
98
|
input_default = str(config.input_path) if config.input_path else ""
|
|
93
99
|
input_result: str = _ask_or_cancel(
|
|
94
100
|
qst_path(
|
|
95
|
-
"Enter the path to the export ZIP, conversations JSON, or extracted directory:",
|
|
101
|
+
"Enter the path to the export ZIP:", # , conversations JSON, or extracted directory:",
|
|
96
102
|
default=input_default,
|
|
97
103
|
validate=_validate_input_path,
|
|
98
104
|
style=CUSTOM_STYLE,
|
|
@@ -118,9 +124,12 @@ def run_interactive_config(initial_config: ConvovizConfig | None = None) -> Conv
|
|
|
118
124
|
|
|
119
125
|
# Prompt for outputs to generate
|
|
120
126
|
output_choices = [
|
|
121
|
-
Choice(
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
Choice(
|
|
128
|
+
title=OUTPUT_TITLES.get(kind, kind.value.title()),
|
|
129
|
+
value=kind,
|
|
130
|
+
checked=kind in config.outputs,
|
|
131
|
+
)
|
|
132
|
+
for kind in OutputKind
|
|
124
133
|
]
|
|
125
134
|
|
|
126
135
|
selected_outputs: list[OutputKind] = _ask_or_cancel(
|
|
@@ -172,20 +181,9 @@ def run_interactive_config(initial_config: ConvovizConfig | None = None) -> Conv
|
|
|
172
181
|
|
|
173
182
|
# Prompt for YAML headers
|
|
174
183
|
yaml_config = config.conversation.yaml
|
|
184
|
+
yaml_fields = list(YAMLConfig.model_fields.keys())
|
|
175
185
|
yaml_choices = [
|
|
176
|
-
Choice(title=field, checked=getattr(yaml_config, field))
|
|
177
|
-
for field in [
|
|
178
|
-
"title",
|
|
179
|
-
"tags",
|
|
180
|
-
"chat_link",
|
|
181
|
-
"create_time",
|
|
182
|
-
"update_time",
|
|
183
|
-
"model",
|
|
184
|
-
"used_plugins",
|
|
185
|
-
"message_count",
|
|
186
|
-
"content_types",
|
|
187
|
-
"custom_instructions",
|
|
188
|
-
]
|
|
186
|
+
Choice(title=field, checked=getattr(yaml_config, field)) for field in yaml_fields
|
|
189
187
|
]
|
|
190
188
|
|
|
191
189
|
selected: list[str] = _ask_or_cancel(
|
|
@@ -197,18 +195,7 @@ def run_interactive_config(initial_config: ConvovizConfig | None = None) -> Conv
|
|
|
197
195
|
)
|
|
198
196
|
|
|
199
197
|
selected_set = set(selected)
|
|
200
|
-
for field_name in
|
|
201
|
-
"title",
|
|
202
|
-
"tags",
|
|
203
|
-
"chat_link",
|
|
204
|
-
"create_time",
|
|
205
|
-
"update_time",
|
|
206
|
-
"model",
|
|
207
|
-
"used_plugins",
|
|
208
|
-
"message_count",
|
|
209
|
-
"content_types",
|
|
210
|
-
"custom_instructions",
|
|
211
|
-
]:
|
|
198
|
+
for field_name in yaml_fields:
|
|
212
199
|
setattr(yaml_config, field_name, field_name in selected_set)
|
|
213
200
|
|
|
214
201
|
# Prompt for wordcloud settings (only if wordclouds output is selected)
|
convoviz/models/conversation.py
CHANGED
|
@@ -24,6 +24,8 @@ class Conversation(BaseModel):
|
|
|
24
24
|
mapping: dict[str, Node]
|
|
25
25
|
moderation_results: list[Any] = Field(default_factory=list)
|
|
26
26
|
current_node: str
|
|
27
|
+
is_starred: bool | None = None
|
|
28
|
+
voice: str | dict[str, Any] | None = None
|
|
27
29
|
plugin_ids: list[str] | None = None
|
|
28
30
|
conversation_id: str
|
|
29
31
|
conversation_template_id: str | None = None
|
|
@@ -156,3 +158,19 @@ class Conversation(BaseModel):
|
|
|
156
158
|
def year_start(self) -> datetime:
|
|
157
159
|
"""Get January 1st of the year this conversation was created."""
|
|
158
160
|
return self.create_time.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def citation_map(self) -> dict[str, dict[str, str | None]]:
|
|
164
|
+
"""Aggregate citation metadata from all messages in the conversation.
|
|
165
|
+
|
|
166
|
+
Traverses all nodes (including hidden ones) to collect embedded citation definitions
|
|
167
|
+
from tool outputs (e.g. search results).
|
|
168
|
+
"""
|
|
169
|
+
aggregated_map = {}
|
|
170
|
+
for node in self.all_message_nodes:
|
|
171
|
+
if not node.message:
|
|
172
|
+
continue
|
|
173
|
+
# Extract citations from message parts
|
|
174
|
+
if hasattr(node.message, "internal_citation_map"):
|
|
175
|
+
aggregated_map.update(node.message.internal_citation_map)
|
|
176
|
+
return aggregated_map
|
convoviz/models/message.py
CHANGED
|
@@ -46,6 +46,9 @@ class MessageMetadata(BaseModel):
|
|
|
46
46
|
is_user_system_message: bool | None = None
|
|
47
47
|
is_visually_hidden_from_conversation: bool | None = None
|
|
48
48
|
user_context_message_data: dict[str, Any] | None = None
|
|
49
|
+
citations: list[dict[str, Any]] | None = None
|
|
50
|
+
search_result_groups: list[dict[str, Any]] | None = None
|
|
51
|
+
content_references: list[dict[str, Any]] | None = None
|
|
49
52
|
|
|
50
53
|
model_config = ConfigDict(protected_namespaces=())
|
|
51
54
|
|
|
@@ -179,11 +182,12 @@ class Message(BaseModel):
|
|
|
179
182
|
1. It is empty (no text, no images).
|
|
180
183
|
2. Explicitly marked as visually hidden.
|
|
181
184
|
3. It is an internal system message (not custom instructions).
|
|
182
|
-
4. It is a browser tool output (intermediate search steps).
|
|
185
|
+
4. It is a browser tool output (intermediate search steps) UNLESS it is a tether_quote.
|
|
183
186
|
5. It is an assistant message targeting a tool (internal call).
|
|
184
187
|
6. It is code interpreter input (content_type="code").
|
|
185
|
-
7. It is browsing status (
|
|
186
|
-
8. It is
|
|
188
|
+
7. It is browsing status, internal reasoning (o1/o3), or massive web scraps (sonic_webpage).
|
|
189
|
+
8. It is a redundant DALL-E textual status update.
|
|
190
|
+
9. It is from internal bio (memory) or web.run orchestration tools.
|
|
187
191
|
"""
|
|
188
192
|
if self.is_empty:
|
|
189
193
|
return True
|
|
@@ -197,10 +201,29 @@ class Message(BaseModel):
|
|
|
197
201
|
# Only show if explicitly marked as user system message (Custom Instructions)
|
|
198
202
|
return not self.metadata.is_user_system_message
|
|
199
203
|
|
|
200
|
-
# Hide
|
|
201
|
-
if self.
|
|
204
|
+
# Hide sonic_webpage (massive scraped text) and system_error
|
|
205
|
+
if self.content.content_type in ("sonic_webpage", "system_error"):
|
|
202
206
|
return True
|
|
203
207
|
|
|
208
|
+
if self.author.role == "tool":
|
|
209
|
+
# Hide memory updates (bio) and internal search orchestration (web.run)
|
|
210
|
+
if self.author.name in ("bio", "web.run"):
|
|
211
|
+
return True
|
|
212
|
+
|
|
213
|
+
# Hide browser tool outputs (intermediate search steps)
|
|
214
|
+
# EXCEPTION: tether_quote (citations) should remain visible
|
|
215
|
+
if self.author.name == "browser":
|
|
216
|
+
return self.content.content_type != "tether_quote"
|
|
217
|
+
|
|
218
|
+
# Hide DALL-E textual status ("DALL·E displayed 1 images...")
|
|
219
|
+
if (
|
|
220
|
+
self.author.name == "dalle.text2im"
|
|
221
|
+
and self.content.content_type == "text"
|
|
222
|
+
# Check if it doesn't have images (just in case they attach images to text logic)
|
|
223
|
+
and not self.images
|
|
224
|
+
):
|
|
225
|
+
return True
|
|
226
|
+
|
|
204
227
|
# Hide assistant messages targeting tools (e.g., search(...), code input)
|
|
205
228
|
# recipient="all" or None means it's for the user; anything else is internal
|
|
206
229
|
if self.author.role == "assistant" and self.recipient not in ("all", None):
|
|
@@ -216,3 +239,56 @@ class Message(BaseModel):
|
|
|
216
239
|
"thoughts",
|
|
217
240
|
"reasoning_recap",
|
|
218
241
|
)
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def internal_citation_map(self) -> dict[str, dict[str, str | None]]:
|
|
245
|
+
"""Extract a map of citation IDs to metadata from content parts.
|
|
246
|
+
|
|
247
|
+
Used for resolving embedded citations (e.g. citeturn0search18).
|
|
248
|
+
Key format: "turn{turn_index}search{ref_index}"
|
|
249
|
+
"""
|
|
250
|
+
if not self.content.parts:
|
|
251
|
+
return {}
|
|
252
|
+
|
|
253
|
+
citation_mapping = {}
|
|
254
|
+
|
|
255
|
+
# Helper to process a single search result entry
|
|
256
|
+
def process_entry(entry: dict[str, Any]) -> None:
|
|
257
|
+
ref_id = entry.get("ref_id")
|
|
258
|
+
if not ref_id:
|
|
259
|
+
return
|
|
260
|
+
# Only care about search results for now
|
|
261
|
+
if ref_id.get("ref_type") != "search":
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
turn_idx = ref_id.get("turn_index")
|
|
265
|
+
ref_idx = ref_id.get("ref_index")
|
|
266
|
+
|
|
267
|
+
if turn_idx is not None and ref_idx is not None:
|
|
268
|
+
# turn_idx is int, ref_idx is int
|
|
269
|
+
key = f"turn{turn_idx}search{ref_idx}"
|
|
270
|
+
citation_mapping[key] = {
|
|
271
|
+
"title": entry.get("title"),
|
|
272
|
+
"url": entry.get("url"),
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# 1. Extract from self.content.parts
|
|
276
|
+
if self.content and self.content.parts:
|
|
277
|
+
for part in self.content.parts:
|
|
278
|
+
if isinstance(part, dict):
|
|
279
|
+
if part.get("type") == "search_result":
|
|
280
|
+
process_entry(part)
|
|
281
|
+
elif part.get("type") == "search_result_group":
|
|
282
|
+
for entry in part.get("entries", []):
|
|
283
|
+
process_entry(entry)
|
|
284
|
+
|
|
285
|
+
# 2. Extract from metadata.search_result_groups (if present)
|
|
286
|
+
if self.metadata and self.metadata.search_result_groups:
|
|
287
|
+
for group in self.metadata.search_result_groups:
|
|
288
|
+
if isinstance(group, dict):
|
|
289
|
+
# Groups might have 'entries' or be flat?
|
|
290
|
+
# Based on name 'groups', likely similar to part structure
|
|
291
|
+
for entry in group.get("entries", []):
|
|
292
|
+
process_entry(entry)
|
|
293
|
+
|
|
294
|
+
return citation_mapping
|
convoviz/renderers/markdown.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
from convoviz.config import AuthorHeaders, ConversationConfig
|
|
7
8
|
from convoviz.exceptions import MessageContentError
|
|
@@ -9,6 +10,82 @@ from convoviz.models import Conversation, Node
|
|
|
9
10
|
from convoviz.renderers.yaml import render_yaml_header
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
def replace_citations(
|
|
14
|
+
text: str,
|
|
15
|
+
citations: list[dict[str, Any]] | None = None,
|
|
16
|
+
citation_map: dict[str, dict[str, str | None]] | None = None,
|
|
17
|
+
) -> str:
|
|
18
|
+
"""Replace citation placeholders in text with markdown links.
|
|
19
|
+
|
|
20
|
+
Supports two formats:
|
|
21
|
+
1. Tether v4 (metadata.citations): Placed at specific indices (【...】 placeholders).
|
|
22
|
+
2. Embedded (Tether v3?): Unicode markers citeturnXsearchY.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
text: The original message text
|
|
26
|
+
citations: List of tether v4 citation objects (start_ix/end_ix)
|
|
27
|
+
citation_map: Map of internal citation IDs to metadata (turnXsearchY -> {title, url})
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Text with all placeholders replaced by markdown links
|
|
31
|
+
"""
|
|
32
|
+
# 1. Handle Tether v4 (Index-based replacements)
|
|
33
|
+
if citations:
|
|
34
|
+
# Sort citations by start_ix descending to replace safely from end
|
|
35
|
+
sorted_citations = sorted(citations, key=lambda c: c.get("start_ix", 0), reverse=True)
|
|
36
|
+
|
|
37
|
+
for cit in sorted_citations:
|
|
38
|
+
start = cit.get("start_ix")
|
|
39
|
+
end = cit.get("end_ix")
|
|
40
|
+
meta = cit.get("metadata", {})
|
|
41
|
+
|
|
42
|
+
if start is None or end is None:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
replacement = _format_link(meta.get("title"), meta.get("url"))
|
|
46
|
+
|
|
47
|
+
# Only replace if strictly positive indices and bounds check
|
|
48
|
+
if 0 <= start < end <= len(text):
|
|
49
|
+
text = text[:start] + replacement + text[end:]
|
|
50
|
+
|
|
51
|
+
# 2. Handle Embedded Citations (Regex-based)
|
|
52
|
+
# Pattern: cite (key)+
|
|
53
|
+
# Codepoints: \uE200 (Start), \uE202 (Sep), \uE201 (End)
|
|
54
|
+
if citation_map is not None:
|
|
55
|
+
pattern = re.compile(r"\uE200cite((?:\uE202[a-zA-Z0-9]+)+)\uE201")
|
|
56
|
+
|
|
57
|
+
def replacer(match: re.Match) -> str:
|
|
58
|
+
# Group 1 contains string like: turn0search18turn0search3
|
|
59
|
+
# Split by separator \uE202 (first item will be empty string)
|
|
60
|
+
raw_keys = match.group(1).split("\ue202")
|
|
61
|
+
keys = [k for k in raw_keys if k]
|
|
62
|
+
|
|
63
|
+
links = []
|
|
64
|
+
for key in keys:
|
|
65
|
+
if key in citation_map:
|
|
66
|
+
data = citation_map[key]
|
|
67
|
+
link = _format_link(data.get("title"), data.get("url"))
|
|
68
|
+
if link:
|
|
69
|
+
links.append(link)
|
|
70
|
+
|
|
71
|
+
return "".join(links)
|
|
72
|
+
|
|
73
|
+
text = pattern.sub(replacer, text)
|
|
74
|
+
|
|
75
|
+
return text
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _format_link(title: str | None, url: str | None) -> str:
|
|
79
|
+
"""Format a title and URL into a concise markdown link."""
|
|
80
|
+
if title and url:
|
|
81
|
+
return f" [[{title}]({url})]"
|
|
82
|
+
elif url:
|
|
83
|
+
return f" [[Source]({url})]"
|
|
84
|
+
elif title:
|
|
85
|
+
return f" [{title}]"
|
|
86
|
+
return ""
|
|
87
|
+
|
|
88
|
+
|
|
12
89
|
def close_code_blocks(text: str) -> str:
|
|
13
90
|
"""Ensure all code blocks in the text are properly closed.
|
|
14
91
|
|
|
@@ -137,6 +214,7 @@ def render_node(
|
|
|
137
214
|
use_dollar_latex: bool = False,
|
|
138
215
|
asset_resolver: Callable[[str], str | None] | None = None,
|
|
139
216
|
flavor: str = "standard",
|
|
217
|
+
citation_map: dict[str, dict[str, str | None]] | None = None,
|
|
140
218
|
) -> str:
|
|
141
219
|
"""Render a complete node as markdown.
|
|
142
220
|
|
|
@@ -146,9 +224,7 @@ def render_node(
|
|
|
146
224
|
use_dollar_latex: Whether to convert LaTeX delimiters to dollars
|
|
147
225
|
asset_resolver: Function to resolve asset IDs to paths
|
|
148
226
|
flavor: Markdown flavor ("standard" or "obsidian")
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
Complete markdown string for the node
|
|
227
|
+
citation_map: Global map of citations
|
|
152
228
|
"""
|
|
153
229
|
if node.message is None:
|
|
154
230
|
return ""
|
|
@@ -185,6 +261,19 @@ def render_node(
|
|
|
185
261
|
# Some message types only contain non-text parts; those still may have images.
|
|
186
262
|
text = ""
|
|
187
263
|
|
|
264
|
+
# Process citations if present (Tether v4 metadata or Embedded v3)
|
|
265
|
+
# Use global citation_map if provided, merging/falling back to local if needed.
|
|
266
|
+
# Actually, local internal map is subset of global map if we aggregated correctly.
|
|
267
|
+
# So we prefer the passed global map.
|
|
268
|
+
effective_map = citation_map or node.message.internal_citation_map
|
|
269
|
+
|
|
270
|
+
if node.message.metadata.citations or effective_map:
|
|
271
|
+
text = replace_citations(
|
|
272
|
+
text,
|
|
273
|
+
citations=node.message.metadata.citations,
|
|
274
|
+
citation_map=effective_map,
|
|
275
|
+
)
|
|
276
|
+
|
|
188
277
|
content = close_code_blocks(text)
|
|
189
278
|
content = f"\n{content}\n" if content else ""
|
|
190
279
|
if use_dollar_latex:
|
|
@@ -255,6 +344,9 @@ def render_conversation(
|
|
|
255
344
|
# Start with YAML header
|
|
256
345
|
markdown = render_yaml_header(conversation, config.yaml)
|
|
257
346
|
|
|
347
|
+
# Pre-calculate citation map for the conversation
|
|
348
|
+
citation_map = conversation.citation_map
|
|
349
|
+
|
|
258
350
|
# Render message nodes in a deterministic traversal order.
|
|
259
351
|
for node in _ordered_nodes(conversation):
|
|
260
352
|
if node.message:
|
|
@@ -264,6 +356,7 @@ def render_conversation(
|
|
|
264
356
|
use_dollar_latex,
|
|
265
357
|
asset_resolver=asset_resolver,
|
|
266
358
|
flavor=flavor,
|
|
359
|
+
citation_map=citation_map,
|
|
267
360
|
)
|
|
268
361
|
|
|
269
362
|
return markdown
|
convoviz/renderers/yaml.py
CHANGED
|
@@ -111,6 +111,10 @@ def render_yaml_header(conversation: Conversation, config: YAMLConfig) -> str:
|
|
|
111
111
|
yaml_fields["content_types"] = conversation.content_types
|
|
112
112
|
if config.custom_instructions:
|
|
113
113
|
yaml_fields["custom_instructions"] = conversation.custom_instructions
|
|
114
|
+
if config.is_starred:
|
|
115
|
+
yaml_fields["is_starred"] = conversation.is_starred
|
|
116
|
+
if config.voice:
|
|
117
|
+
yaml_fields["voice"] = conversation.voice
|
|
114
118
|
|
|
115
119
|
if not yaml_fields:
|
|
116
120
|
return ""
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: convoviz
|
|
3
|
-
Version: 0.4.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.4.7
|
|
4
|
+
Summary: Convert your ChatGPT export (ZIP) into clean Markdown text files with inline media, and generate data visualizations like word clouds and usage graphs.
|
|
5
5
|
Keywords: markdown,chatgpt,openai,visualization,analytics,json,export,data-analysis,obsidian
|
|
6
6
|
Author: Mohamed Cheikh Sidiya
|
|
7
7
|
Author-email: Mohamed Cheikh Sidiya <mohamedcheikhsidiya77@gmail.com>
|
|
@@ -27,15 +27,21 @@ Description-Content-Type: text/markdown
|
|
|
27
27
|
<h1 align="center">Convoviz 📊</h1>
|
|
28
28
|
<p align="center"><strong>Visualize your entire ChatGPT data</strong></p>
|
|
29
29
|
<p align="center">
|
|
30
|
-
Convert your ChatGPT history into
|
|
30
|
+
Convert your ChatGPT history into clean, readable Markdown (text files).
|
|
31
|
+
</p>
|
|
32
|
+
<p align="center"><strong>
|
|
33
|
+
Perfect for archiving, local search, or use with note-taking apps like Obsidian.
|
|
34
|
+
</strong></p>
|
|
35
|
+
<p align="center">
|
|
31
36
|
Visualize your data with word clouds 🔡☁️ and usage graphs 📈.
|
|
32
37
|
</p>
|
|
33
38
|
</p>
|
|
34
39
|
|
|
35
40
|
<p align="center">
|
|
36
|
-
<a href="https://pypi.org/project/convoviz/"><img src="https://img.shields.io/pypi/v/convoviz?style=
|
|
37
|
-
<a href="https://
|
|
38
|
-
<a href="https://
|
|
41
|
+
<a href="https://pypi.org/project/convoviz/"><img src="https://img.shields.io/pypi/v/convoviz?style=for-the-badge&logo=python&logoColor=white" alt="PyPI Version"></a>
|
|
42
|
+
<a href="https://github.com/mohamed-chs/convoviz/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/convoviz?style=for-the-badge" alt="License"></a>
|
|
43
|
+
<a href="https://pepy.tech/projects/convoviz"><img src="https://img.shields.io/pepy/dt/convoviz?style=for-the-badge&color=blue" alt="Downloads"></a>
|
|
44
|
+
<a href="https://github.com/mohamed-chs/convoviz/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/mohamed-chs/convoviz/ci.yml?style=for-the-badge&logo=github&label=CI" alt="CI Status"></a>
|
|
39
45
|
</p>
|
|
40
46
|
|
|
41
47
|
---
|
|
@@ -49,7 +55,7 @@ Description-Content-Type: text/markdown
|
|
|
49
55
|
| ☁️ **Word Clouds** | Visual breakdowns of your most-used words and phrases |
|
|
50
56
|
| 📈 **Usage Graphs** | Bar plots and charts showing your conversation patterns |
|
|
51
57
|
|
|
52
|
-
> 💡 **See examples in the [`demo/`](demo) folder!**
|
|
58
|
+
> 💡 **See examples in the [`demo/`](https://github.com/mohamed-chs/convoviz/tree/main/demo) folder!**
|
|
53
59
|
|
|
54
60
|
---
|
|
55
61
|
|
|
@@ -66,25 +72,29 @@ Description-Content-Type: text/markdown
|
|
|
66
72
|
|
|
67
73
|
### Step 2: Install Convoviz
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
<summary><strong>🚀 Quick Install (Recommended)</strong></summary>
|
|
75
|
+
### 🚀 Quick Install
|
|
71
76
|
|
|
72
|
-
|
|
77
|
+
Run one of the commands below to install **everything** you need automatically.
|
|
73
78
|
|
|
74
|
-
|
|
79
|
+
#### 🍎 macOS / 🐧 Linux
|
|
80
|
+
1. Open `Terminal`.
|
|
81
|
+
- **macOS**: Press `Cmd + Space`, type "Terminal", and hit Enter.
|
|
82
|
+
- **Linux**: Press `Ctrl + Alt + T`, or search "Terminal" in your app menu.
|
|
83
|
+
2. Copy and paste this command:
|
|
75
84
|
|
|
76
85
|
```bash
|
|
77
86
|
curl -fsSL https://raw.githubusercontent.com/mohamed-chs/convoviz/main/install.sh | bash
|
|
78
87
|
```
|
|
79
88
|
|
|
80
|
-
|
|
89
|
+
#### 🪟 Windows
|
|
90
|
+
1. Open `PowerShell`.
|
|
91
|
+
- Press the `Windows` key, type "PowerShell", and hit Enter.
|
|
92
|
+
2. Copy and paste this command:
|
|
81
93
|
|
|
82
94
|
```powershell
|
|
83
95
|
irm https://raw.githubusercontent.com/mohamed-chs/convoviz/main/install.ps1 | iex
|
|
84
96
|
```
|
|
85
97
|
|
|
86
|
-
</details>
|
|
87
|
-
|
|
88
98
|
<details>
|
|
89
99
|
<summary><strong>📦 Alternative: Install with pip</strong></summary>
|
|
90
100
|
|
|
@@ -107,7 +117,7 @@ pip install "convoviz[viz]"
|
|
|
107
117
|
|
|
108
118
|
### Step 3: Run Convoviz
|
|
109
119
|
|
|
110
|
-
The simplest way is to run
|
|
120
|
+
The simplest way is to run this in your terminal and follow the interactive prompts:
|
|
111
121
|
|
|
112
122
|
```bash
|
|
113
123
|
convoviz
|
|
@@ -156,11 +166,13 @@ convoviz --help
|
|
|
156
166
|
|
|
157
167
|
### Step 4: Check the Output 🎉
|
|
158
168
|
|
|
159
|
-
After running the script, head to your output folder to see:
|
|
169
|
+
After running the script, head to your output folder (defaults to `Documents/ChatGPT-Data` if you didn't change it) to see:
|
|
160
170
|
- 📝 Neatly formatted Markdown files
|
|
161
171
|
- 📊 Visualizations and graphs
|
|
162
172
|
|
|
163
|
-
!
|
|
173
|
+
If you've had a great experience, consider giving the project a ⭐ **star**! It keeps me motivated and helps others discover it!
|
|
174
|
+
|
|
175
|
+

|
|
164
176
|
|
|
165
177
|
---
|
|
166
178
|
|
|
@@ -172,12 +184,29 @@ Whether you're a tech wizard or you're new to all this, I'd love to hear about y
|
|
|
172
184
|
|
|
173
185
|
👉 **[Open an Issue](https://github.com/mohamed-chs/convoviz/issues)**
|
|
174
186
|
|
|
175
|
-
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 🤝 Contributing
|
|
190
|
+
|
|
191
|
+
Interested in contributing? Check out the **[Contributing Guide](https://github.com/mohamed-chs/convoviz/tree/main/CONTRIBUTING.md)** for development setup, code style, and how to submit a pull request.
|
|
176
192
|
|
|
177
193
|
---
|
|
178
194
|
|
|
179
195
|
## 📝 Notes
|
|
180
196
|
|
|
197
|
+
<details>
|
|
198
|
+
<summary><strong>Offline</strong></summary>
|
|
199
|
+
|
|
200
|
+
Word clouds use NLTK stopwords. If you're offline and NLTK data isn't installed yet, pre-download it:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
python -c "import nltk; nltk.download('stopwords')"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**NOTE:** The install script already handles this, so you can immediately go offline after running it.
|
|
207
|
+
|
|
208
|
+
</details>
|
|
209
|
+
|
|
181
210
|
<details>
|
|
182
211
|
<summary><strong>About This Project</strong></summary>
|
|
183
212
|
|
|
@@ -196,17 +225,6 @@ It should also work as a library, so you can import and use the models and funct
|
|
|
196
225
|
|
|
197
226
|
</details>
|
|
198
227
|
|
|
199
|
-
<details>
|
|
200
|
-
<summary><strong>Offline / Reproducible Runs</strong></summary>
|
|
201
|
-
|
|
202
|
-
Word clouds use NLTK stopwords. If you're offline and NLTK data isn't installed yet, pre-download it:
|
|
203
|
-
|
|
204
|
-
```bash
|
|
205
|
-
python -c "import nltk; nltk.download('stopwords')"
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
</details>
|
|
209
|
-
|
|
210
228
|
<details>
|
|
211
229
|
<summary><strong>Bookmarklet (Experimental)</strong></summary>
|
|
212
230
|
|
|
@@ -36,10 +36,10 @@ 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=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
39
|
-
convoviz/cli.py,sha256=
|
|
40
|
-
convoviz/config.py,sha256=
|
|
39
|
+
convoviz/cli.py,sha256=nniH7QPbbH_buQJGa35vd3IEl7RvZsWRiLpUuhlxXaI,5314
|
|
40
|
+
convoviz/config.py,sha256=4L0gSOYUPWPEif6lJM1VhkkJq7rAZwAkMi5DIv1Pkwc,3677
|
|
41
41
|
convoviz/exceptions.py,sha256=bQpIKls48uOQpagEJAxpXf5LF7QoagRRfbD0MjWC7Ak,1476
|
|
42
|
-
convoviz/interactive.py,sha256=
|
|
42
|
+
convoviz/interactive.py,sha256=z4Xdhk_47R1Zx_CaPpY_Gq88i6l9A8YKN3mlc5Uz6KU,8284
|
|
43
43
|
convoviz/io/__init__.py,sha256=y70TYypJ36_kaEA04E2wa1EDaKQVjprKItoKR6MMs4M,471
|
|
44
44
|
convoviz/io/assets.py,sha256=5zcZPlQa9niDw9o-sqJIKgLc0OJ9auzd6KAve5WfBkQ,3459
|
|
45
45
|
convoviz/io/loaders.py,sha256=SqmBWUBqT5lsCf01yy-FUhwIxbiKTFMQnj4k213DsGI,5891
|
|
@@ -47,16 +47,16 @@ convoviz/io/writers.py,sha256=-HTvj7D9sqM8M-RsGwd44AquxCVmcDVHgta22QlfNzU,7068
|
|
|
47
47
|
convoviz/logging_config.py,sha256=PRuOKij8UD6sKdg3lAsu9lUsTUZ3O6_6uffnyg07M1U,2060
|
|
48
48
|
convoviz/models/__init__.py,sha256=6gAfrk6KJT2QxdvX_v15mUdfIqEw1xKxwQlKSfyA5eI,532
|
|
49
49
|
convoviz/models/collection.py,sha256=L658yKMNC6IZrfxYxZBe-oO9COP_bzVfRznnNon7tfU,4467
|
|
50
|
-
convoviz/models/conversation.py,sha256=
|
|
51
|
-
convoviz/models/message.py,sha256=
|
|
50
|
+
convoviz/models/conversation.py,sha256=IZvDMXxbHSW3Hvxljm8ZpB5eJceJkJ3prDUvZOtrKyM,6419
|
|
51
|
+
convoviz/models/message.py,sha256=lJV51fVLaiIamcTG96VyVq5Khluyp6E_87BWynbxUXg,11591
|
|
52
52
|
convoviz/models/node.py,sha256=1vBAtKVscYsUBDnKAOyLxuZaK9KoVF1dFXiKXRHxUnY,1946
|
|
53
53
|
convoviz/pipeline.py,sha256=1kLtsNDN3LYNudyPBlyKwQZ8zWCmRKveP3VWfIgichw,6765
|
|
54
54
|
convoviz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
55
|
convoviz/renderers/__init__.py,sha256=IQgwD9NqtUgbS9zwyPBNZbBIZcFrbZ9C7WMAV-X3Xdg,261
|
|
56
|
-
convoviz/renderers/markdown.py,sha256=
|
|
57
|
-
convoviz/renderers/yaml.py,sha256=
|
|
56
|
+
convoviz/renderers/markdown.py,sha256=uv6SshqY6Nuj374I8qpRXQSlCJ7pLF0IUBl0y-Nd3so,11323
|
|
57
|
+
convoviz/renderers/yaml.py,sha256=R6hjXCpgeVm3rPuPVgaj2VopfpPqRFxAFWY7Nxtf6Vg,4213
|
|
58
58
|
convoviz/utils.py,sha256=IQEKYHhWOnYxlr4GwAHoquG0BXTlVRkORL80oUSaIeQ,3417
|
|
59
|
-
convoviz-0.4.
|
|
60
|
-
convoviz-0.4.
|
|
61
|
-
convoviz-0.4.
|
|
62
|
-
convoviz-0.4.
|
|
59
|
+
convoviz-0.4.7.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
60
|
+
convoviz-0.4.7.dist-info/entry_points.txt,sha256=HYsmsw5vt36yYHB05uVU48AK2WLkcwshly7m7KKuZMY,54
|
|
61
|
+
convoviz-0.4.7.dist-info/METADATA,sha256=VbjYg-utjxShJNRT4p3Pf1Lp37TyJcE4FZbUzDE1j3s,7883
|
|
62
|
+
convoviz-0.4.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|