session-slides 0.2.0 → 0.2.2
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.
- package/README.md +0 -1
- package/package.json +5 -1
- package/scripts/generate_slides.py +4 -23
- package/scripts/html_generator.py +33 -40
- package/scripts/titles.py +0 -78
package/README.md
CHANGED
|
@@ -36,7 +36,6 @@ Output defaults to `./session-slides/{timestamp}.html` in your current directory
|
|
|
36
36
|
| `--output PATH` | Output HTML file path (default: `./session-slides/{timestamp}.html`) |
|
|
37
37
|
| `--title TEXT` | Custom presentation title |
|
|
38
38
|
| `--open` | Open in browser after generation |
|
|
39
|
-
| `--ai-titles` | Use Ollama for slide titles (requires local Ollama) |
|
|
40
39
|
| `--clean` | Remove previous timestamped output files |
|
|
41
40
|
| `--verbose` | Enable verbose output |
|
|
42
41
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "session-slides",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Convert Claude Code session transcripts into navigable HTML slide presentations",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"linux",
|
|
34
34
|
"win32"
|
|
35
35
|
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "python3 -m pytest scripts/tests/ -q",
|
|
38
|
+
"prepublishOnly": "python3 -m pytest scripts/tests/ -q"
|
|
39
|
+
},
|
|
36
40
|
"dependencies": {
|
|
37
41
|
"cross-spawn": "^7.0.3"
|
|
38
42
|
}
|
|
@@ -12,7 +12,6 @@ Options:
|
|
|
12
12
|
--output PATH Output HTML file (default: ./session-slides/{timestamp}.html)
|
|
13
13
|
--title TEXT Custom presentation title
|
|
14
14
|
--open Open in browser after generation
|
|
15
|
-
--ai-titles Use Ollama for title generation (requires ollama)
|
|
16
15
|
--clean Remove previous output files before generating
|
|
17
16
|
"""
|
|
18
17
|
|
|
@@ -25,7 +24,7 @@ from typing import Optional
|
|
|
25
24
|
|
|
26
25
|
# Import from sibling modules
|
|
27
26
|
from parser import Session, Turn, extract_turns, find_current_session, load_session
|
|
28
|
-
from titles import generate_turn_title, generate_continued_title
|
|
27
|
+
from titles import generate_turn_title, generate_continued_title
|
|
29
28
|
from truncation import (
|
|
30
29
|
TruncationConfig,
|
|
31
30
|
truncate_user_prompt,
|
|
@@ -51,22 +50,14 @@ def print_error(message: str) -> None:
|
|
|
51
50
|
print(f"[✗] {message}", file=sys.stderr)
|
|
52
51
|
|
|
53
52
|
|
|
54
|
-
def generate_titles_for_session(session: Session
|
|
53
|
+
def generate_titles_for_session(session: Session) -> dict[int, str]:
|
|
55
54
|
"""Generate titles for all turns in a session."""
|
|
56
55
|
titles = {}
|
|
57
56
|
|
|
58
57
|
for turn in session.turns:
|
|
59
58
|
if turn.is_user_message():
|
|
60
59
|
prompt = turn.get_text_content()
|
|
61
|
-
if
|
|
62
|
-
# Try AI first, fall back to heuristic
|
|
63
|
-
ai_title = generate_title_ollama(prompt)
|
|
64
|
-
if ai_title:
|
|
65
|
-
titles[turn.number if hasattr(turn, 'number') else len(titles) + 1] = ai_title
|
|
66
|
-
else:
|
|
67
|
-
titles[turn.number if hasattr(turn, 'number') else len(titles) + 1] = generate_turn_title(prompt, len(titles) + 1)
|
|
68
|
-
else:
|
|
69
|
-
titles[turn.number if hasattr(turn, 'number') else len(titles) + 1] = generate_turn_title(prompt, len(titles) + 1)
|
|
60
|
+
titles[turn.number if hasattr(turn, 'number') else len(titles) + 1] = generate_turn_title(prompt, len(titles) + 1)
|
|
70
61
|
|
|
71
62
|
return titles
|
|
72
63
|
|
|
@@ -159,7 +150,6 @@ def main() -> int:
|
|
|
159
150
|
Examples:
|
|
160
151
|
python generate_slides.py --from session.jsonl
|
|
161
152
|
python generate_slides.py --from session.jsonl --output slides.html --title "My Session"
|
|
162
|
-
python generate_slides.py --from session.jsonl --ai-titles --open
|
|
163
153
|
python generate_slides.py # Auto-detect latest session file
|
|
164
154
|
""",
|
|
165
155
|
)
|
|
@@ -194,12 +184,6 @@ Examples:
|
|
|
194
184
|
help="Open the generated HTML in default browser",
|
|
195
185
|
)
|
|
196
186
|
|
|
197
|
-
parser.add_argument(
|
|
198
|
-
"--ai-titles",
|
|
199
|
-
action="store_true",
|
|
200
|
-
help="Use Ollama AI to generate slide titles (requires local Ollama)",
|
|
201
|
-
)
|
|
202
|
-
|
|
203
187
|
parser.add_argument(
|
|
204
188
|
"--verbose",
|
|
205
189
|
"-v",
|
|
@@ -249,10 +233,7 @@ Examples:
|
|
|
249
233
|
print_success(f"Parsed {len(session.turns)} messages ({user_turns} user turns)")
|
|
250
234
|
|
|
251
235
|
# Step 3: Convert to dict format and generate titles
|
|
252
|
-
|
|
253
|
-
print_progress("Generating AI titles with Ollama...")
|
|
254
|
-
else:
|
|
255
|
-
print_progress("Generating heuristic titles...")
|
|
236
|
+
print_progress("Generating slide titles...")
|
|
256
237
|
|
|
257
238
|
session_dict = session_to_dict(session)
|
|
258
239
|
print_success(f"Generated {session_dict['total_turns']} slide titles")
|
|
@@ -1056,6 +1056,8 @@ def _format_datetime(dt_string: str) -> tuple:
|
|
|
1056
1056
|
"""
|
|
1057
1057
|
Parse and format a datetime string into readable date and time components.
|
|
1058
1058
|
|
|
1059
|
+
Converts UTC timestamps to the local timezone before formatting.
|
|
1060
|
+
|
|
1059
1061
|
Args:
|
|
1060
1062
|
dt_string: ISO format datetime string or similar
|
|
1061
1063
|
|
|
@@ -1065,32 +1067,33 @@ def _format_datetime(dt_string: str) -> tuple:
|
|
|
1065
1067
|
if not dt_string:
|
|
1066
1068
|
return None, None
|
|
1067
1069
|
|
|
1068
|
-
#
|
|
1069
|
-
clean_dt = re.sub(r'[+-]\d{2}:\d{2}$', '', dt_string)
|
|
1070
|
-
|
|
1071
|
-
# Try various datetime formats
|
|
1072
|
-
formats = [
|
|
1073
|
-
'%Y-%m-%dT%H:%M:%S.%fZ',
|
|
1074
|
-
'%Y-%m-%dT%H:%M:%SZ',
|
|
1075
|
-
'%Y-%m-%dT%H:%M:%S.%f',
|
|
1076
|
-
'%Y-%m-%dT%H:%M:%S',
|
|
1077
|
-
'%Y-%m-%d %H:%M:%S.%f',
|
|
1078
|
-
'%Y-%m-%d %H:%M:%S',
|
|
1079
|
-
'%Y-%m-%d',
|
|
1080
|
-
]
|
|
1081
|
-
|
|
1070
|
+
# Try Python's fromisoformat first (handles +00:00, Z, microseconds)
|
|
1082
1071
|
dt = None
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1072
|
+
try:
|
|
1073
|
+
dt = datetime.fromisoformat(dt_string.replace('Z', '+00:00'))
|
|
1074
|
+
except (ValueError, AttributeError):
|
|
1075
|
+
# Fall back to manual parsing for non-standard formats
|
|
1076
|
+
clean_dt = re.sub(r'[+-]\d{2}:\d{2}$', '', dt_string)
|
|
1077
|
+
for fmt in [
|
|
1078
|
+
'%Y-%m-%dT%H:%M:%S.%f',
|
|
1079
|
+
'%Y-%m-%dT%H:%M:%S',
|
|
1080
|
+
'%Y-%m-%d %H:%M:%S.%f',
|
|
1081
|
+
'%Y-%m-%d %H:%M:%S',
|
|
1082
|
+
'%Y-%m-%d',
|
|
1083
|
+
]:
|
|
1084
|
+
try:
|
|
1085
|
+
dt = datetime.strptime(clean_dt, fmt)
|
|
1086
|
+
break
|
|
1087
|
+
except ValueError:
|
|
1088
|
+
continue
|
|
1089
1089
|
|
|
1090
1090
|
if dt is None:
|
|
1091
|
-
# Return the raw string if we can't parse it
|
|
1092
1091
|
return dt_string, None
|
|
1093
1092
|
|
|
1093
|
+
# Convert to local timezone if the datetime is timezone-aware
|
|
1094
|
+
if dt.tzinfo is not None:
|
|
1095
|
+
dt = dt.astimezone()
|
|
1096
|
+
|
|
1094
1097
|
# Format nicely: "February 4, 2026" and "2:30 PM"
|
|
1095
1098
|
date_str = dt.strftime('%B %d, %Y').replace(' 0', ' ') # Remove leading zero from day
|
|
1096
1099
|
time_str = dt.strftime('%I:%M %p').lstrip('0') # Remove leading zero from hour
|
|
@@ -1120,28 +1123,18 @@ def _calculate_duration(turns: list) -> str:
|
|
|
1120
1123
|
if len(timestamps) < 2:
|
|
1121
1124
|
return None
|
|
1122
1125
|
|
|
1123
|
-
#
|
|
1124
|
-
first_ts = re.sub(r'[+-]\d{2}:\d{2}$', '', timestamps[0])
|
|
1125
|
-
last_ts = re.sub(r'[+-]\d{2}:\d{2}$', '', timestamps[-1])
|
|
1126
|
-
|
|
1127
|
-
# Try to parse first and last timestamps
|
|
1128
|
-
formats = [
|
|
1129
|
-
'%Y-%m-%dT%H:%M:%S.%fZ',
|
|
1130
|
-
'%Y-%m-%dT%H:%M:%SZ',
|
|
1131
|
-
'%Y-%m-%dT%H:%M:%S.%f',
|
|
1132
|
-
'%Y-%m-%dT%H:%M:%S',
|
|
1133
|
-
]
|
|
1134
|
-
|
|
1126
|
+
# Parse first and last timestamps
|
|
1135
1127
|
first_dt = None
|
|
1136
1128
|
last_dt = None
|
|
1137
|
-
|
|
1138
|
-
for fmt in formats:
|
|
1129
|
+
for ts_str, target in [(timestamps[0], 'first'), (timestamps[-1], 'last')]:
|
|
1139
1130
|
try:
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1131
|
+
dt = datetime.fromisoformat(ts_str.replace('Z', '+00:00'))
|
|
1132
|
+
except (ValueError, AttributeError):
|
|
1133
|
+
dt = None
|
|
1134
|
+
if target == 'first':
|
|
1135
|
+
first_dt = dt
|
|
1136
|
+
else:
|
|
1137
|
+
last_dt = dt
|
|
1145
1138
|
|
|
1146
1139
|
if first_dt is None or last_dt is None:
|
|
1147
1140
|
return None
|
package/scripts/titles.py
CHANGED
|
@@ -944,84 +944,6 @@ def generate_continued_title(base_title: str) -> str:
|
|
|
944
944
|
return f"{base_title} (continued)"
|
|
945
945
|
|
|
946
946
|
|
|
947
|
-
def generate_title_ollama(
|
|
948
|
-
prompt: str,
|
|
949
|
-
turn_number: int,
|
|
950
|
-
model: str = "llama3.2",
|
|
951
|
-
host: str = "http://localhost:11434",
|
|
952
|
-
timeout: float = 5.0
|
|
953
|
-
) -> str:
|
|
954
|
-
"""
|
|
955
|
-
Generate a title using Ollama for AI-enhanced extraction.
|
|
956
|
-
|
|
957
|
-
Falls back to pattern-based generation if Ollama is unavailable.
|
|
958
|
-
|
|
959
|
-
Args:
|
|
960
|
-
prompt: The user's prompt text
|
|
961
|
-
turn_number: The turn number for fallback
|
|
962
|
-
model: The Ollama model to use
|
|
963
|
-
host: The Ollama API host
|
|
964
|
-
timeout: Request timeout in seconds
|
|
965
|
-
|
|
966
|
-
Returns:
|
|
967
|
-
An AI-generated or pattern-based title
|
|
968
|
-
|
|
969
|
-
Examples:
|
|
970
|
-
>>> # With Ollama running:
|
|
971
|
-
>>> generate_title_ollama("Can you help me create a REST API with authentication?", 1)
|
|
972
|
-
'Creating REST API Authentication'
|
|
973
|
-
|
|
974
|
-
>>> # Without Ollama (falls back to pattern matching):
|
|
975
|
-
>>> generate_title_ollama("fix the broken login", 2)
|
|
976
|
-
'Fixing Broken Login'
|
|
977
|
-
"""
|
|
978
|
-
# Try Ollama first
|
|
979
|
-
try:
|
|
980
|
-
import requests
|
|
981
|
-
|
|
982
|
-
system_prompt = """You are a title generator. Given a user's request, generate a short (2-5 word)
|
|
983
|
-
descriptive title in gerund form (e.g., "Creating Login Form", "Fixing Database Bug").
|
|
984
|
-
|
|
985
|
-
Rules:
|
|
986
|
-
- Start with a gerund (verb ending in -ing)
|
|
987
|
-
- Keep it concise and specific
|
|
988
|
-
- Use title case
|
|
989
|
-
- Do not include punctuation
|
|
990
|
-
- Output ONLY the title, nothing else"""
|
|
991
|
-
|
|
992
|
-
response = requests.post(
|
|
993
|
-
f"{host}/api/generate",
|
|
994
|
-
json={
|
|
995
|
-
"model": model,
|
|
996
|
-
"prompt": f"Generate a title for this request: {prompt}",
|
|
997
|
-
"system": system_prompt,
|
|
998
|
-
"stream": False,
|
|
999
|
-
"options": {
|
|
1000
|
-
"temperature": 0.3,
|
|
1001
|
-
"num_predict": 20,
|
|
1002
|
-
}
|
|
1003
|
-
},
|
|
1004
|
-
timeout=timeout
|
|
1005
|
-
)
|
|
1006
|
-
|
|
1007
|
-
if response.status_code == 200:
|
|
1008
|
-
result = response.json()
|
|
1009
|
-
title = result.get("response", "").strip()
|
|
1010
|
-
|
|
1011
|
-
# Validate the title
|
|
1012
|
-
if title and 2 <= len(title.split()) <= 6:
|
|
1013
|
-
# Clean up any quotes or extra punctuation
|
|
1014
|
-
title = title.strip("\"'.,;:!?")
|
|
1015
|
-
return title
|
|
1016
|
-
|
|
1017
|
-
except Exception:
|
|
1018
|
-
# Ollama not available or error - fall through to pattern matching
|
|
1019
|
-
pass
|
|
1020
|
-
|
|
1021
|
-
# Fallback to pattern-based generation
|
|
1022
|
-
return generate_turn_title(prompt, turn_number)
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
947
|
# Module-level convenience for testing
|
|
1026
948
|
if __name__ == "__main__":
|
|
1027
949
|
# Test examples
|