youread 0.2.0__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.
- youread-0.2.0/LICENSE +21 -0
- youread-0.2.0/MANIFEST.in +12 -0
- youread-0.2.0/PKG-INFO +130 -0
- youread-0.2.0/README.md +103 -0
- youread-0.2.0/pyproject.toml +47 -0
- youread-0.2.0/setup.cfg +4 -0
- youread-0.2.0/src/youread/__init__.py +1 -0
- youread-0.2.0/src/youread/article_generator.py +201 -0
- youread-0.2.0/src/youread/config/settings.yaml +9 -0
- youread-0.2.0/src/youread/exceptions.py +45 -0
- youread-0.2.0/src/youread/main.py +496 -0
- youread-0.2.0/src/youread/metadata.py +83 -0
- youread-0.2.0/src/youread/models/__init__.py +6 -0
- youread-0.2.0/src/youread/models/transcript.py +22 -0
- youread-0.2.0/src/youread/models/video.py +21 -0
- youread-0.2.0/src/youread/prompts/TECHNIQUES_APPLIED.md +271 -0
- youread-0.2.0/src/youread/prompts/__init__.py +1 -0
- youread-0.2.0/src/youread/prompts/examples_v2.md +224 -0
- youread-0.2.0/src/youread/prompts/modes/__init__.py +1 -0
- youread-0.2.0/src/youread/prompts/modes/detailed.md +39 -0
- youread-0.2.0/src/youread/prompts/modes/standard.md +30 -0
- youread-0.2.0/src/youread/prompts/modes/summary.md +30 -0
- youread-0.2.0/src/youread/prompts/system.md +12 -0
- youread-0.2.0/src/youread/prompts/system_prompt_v2.md +259 -0
- youread-0.2.0/src/youread/providers/__init__.py +107 -0
- youread-0.2.0/src/youread/providers/gemini_provider.py +39 -0
- youread-0.2.0/src/youread/providers/openai_provider.py +86 -0
- youread-0.2.0/src/youread/sponsorblock.py +107 -0
- youread-0.2.0/src/youread/transcript.py +104 -0
- youread-0.2.0/src/youread/utils/__init__.py +27 -0
- youread-0.2.0/src/youread/utils/config.py +280 -0
- youread-0.2.0/src/youread/utils/url_parser.py +51 -0
- youread-0.2.0/src/youread.egg-info/PKG-INFO +130 -0
- youread-0.2.0/src/youread.egg-info/SOURCES.txt +39 -0
- youread-0.2.0/src/youread.egg-info/dependency_links.txt +1 -0
- youread-0.2.0/src/youread.egg-info/entry_points.txt +2 -0
- youread-0.2.0/src/youread.egg-info/requires.txt +7 -0
- youread-0.2.0/src/youread.egg-info/top_level.txt +1 -0
- youread-0.2.0/tests/test_config_utils.py +238 -0
- youread-0.2.0/tests/test_main_cli_config.py +148 -0
- youread-0.2.0/tests/test_secret_hygiene.py +147 -0
youread-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Guransh Singh (singhDevs)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
youread-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: youread
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Convert YouTube videos into comprehensive, reading-optimized markdown articles
|
|
5
|
+
Author: Guransh Singh (singhDevs)
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/singhDevs/YouRead
|
|
8
|
+
Project-URL: Issues, https://github.com/singhDevs/YouRead/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Utilities
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: youtube-transcript-api>=0.6.0
|
|
20
|
+
Requires-Dist: google-genai>=1.0.0
|
|
21
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Requires-Dist: typer>=0.9.0
|
|
24
|
+
Requires-Dist: rich>=13.0.0
|
|
25
|
+
Requires-Dist: openai==2.16.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# YouRead
|
|
29
|
+
|
|
30
|
+
Turn a YouTube video into a readable Markdown article.
|
|
31
|
+
|
|
32
|
+
Built for people who prefer reading over watching, who got 100s of youtube tabs opened but none watched — technical talks, podcasts, discussions. It goes beyond summaries; the goal is an article detailed enough that you usually don't need to watch the video.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install .
|
|
39
|
+
youread config set --provider openai --set-api-key
|
|
40
|
+
youread run "https://youtube.com/watch?v=VIDEO_ID"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
Requires Python 3.10+.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install .
|
|
49
|
+
# or for development:
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Configure
|
|
54
|
+
|
|
55
|
+
YouRead supports OpenAI and Gemini. Defaults:
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
llm:
|
|
59
|
+
provider: openai
|
|
60
|
+
model: gpt-5
|
|
61
|
+
max_tokens: 8000
|
|
62
|
+
temperature: 0.3
|
|
63
|
+
output:
|
|
64
|
+
directory: ./output
|
|
65
|
+
processing:
|
|
66
|
+
remove_sponsors: true
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# set provider + model
|
|
71
|
+
youread config set --provider openai --model gpt-5
|
|
72
|
+
# set API key (secure prompt, no echo)
|
|
73
|
+
youread config set --set-api-key
|
|
74
|
+
# view current config
|
|
75
|
+
youread config show
|
|
76
|
+
# check if active provider has a key
|
|
77
|
+
youread config check
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Provider/model pairs are validated at save time — `--provider gemini --model gpt-5` is rejected immediately.
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
youread run "https://youtube.com/watch?v=VIDEO_ID"
|
|
86
|
+
|
|
87
|
+
# with options
|
|
88
|
+
youread run "URL" --mode detailed
|
|
89
|
+
youread run "URL" --prompt "Focus on code examples"
|
|
90
|
+
youread run "URL" --model gemini-2.5-flash
|
|
91
|
+
youread run "URL" --no-sponsorblock
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| Flag | What it does |
|
|
95
|
+
|------|-------------|
|
|
96
|
+
| `--mode summary\|standard\|detailed` | Article depth (default: standard) |
|
|
97
|
+
| `--prompt "..."` | Custom instructions appended to the system prompt |
|
|
98
|
+
| `--model <name>` | Override configured model for one run |
|
|
99
|
+
| `--no-sponsorblock` | Skip sponsor segment removal |
|
|
100
|
+
|
|
101
|
+
Output goes to the configured directory (`./output` by default). After generation, YouRead asks whether to open the file.
|
|
102
|
+
|
|
103
|
+
## Supported Providers
|
|
104
|
+
|
|
105
|
+
| Provider | Example Model | Env variable |
|
|
106
|
+
|----------|---------------|-------------|
|
|
107
|
+
| OpenAI | `gpt-5` | `OPENAI_API_KEY` |
|
|
108
|
+
| Gemini | `gemini-2.5-flash` | `GEMINI_API_KEY` |
|
|
109
|
+
|
|
110
|
+
## Limitations
|
|
111
|
+
|
|
112
|
+
YouRead is transcript-only. It doesn't see video frames, slides, or code on screen.
|
|
113
|
+
|
|
114
|
+
| Situation | Outcome |
|
|
115
|
+
|-----------|---------|
|
|
116
|
+
| No transcript | Can't process |
|
|
117
|
+
| Auto-generated captions | Quality suffers |
|
|
118
|
+
| Visual-heavy content | Details missed |
|
|
119
|
+
| Code not spoken | Won't appear |
|
|
120
|
+
| Non-English video | English captions only |
|
|
121
|
+
|
|
122
|
+
For best results, use videos where the speaker explains things verbally.
|
|
123
|
+
|
|
124
|
+
## Troubleshooting
|
|
125
|
+
|
|
126
|
+
**`No API key found`** — `youread config check` then `youread config set --set-api-key`.
|
|
127
|
+
|
|
128
|
+
**`Provider mismatch`** — You paired `--provider gemini` with `--model gpt-5`. Use matching pairs: `openai` + `gpt-5`, `gemini` + `gemini-2.5-flash`.
|
|
129
|
+
|
|
130
|
+
**`No transcript available`** — Video has no accessible captions.
|
youread-0.2.0/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# YouRead
|
|
2
|
+
|
|
3
|
+
Turn a YouTube video into a readable Markdown article.
|
|
4
|
+
|
|
5
|
+
Built for people who prefer reading over watching, who got 100s of youtube tabs opened but none watched — technical talks, podcasts, discussions. It goes beyond summaries; the goal is an article detailed enough that you usually don't need to watch the video.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install .
|
|
12
|
+
youread config set --provider openai --set-api-key
|
|
13
|
+
youread run "https://youtube.com/watch?v=VIDEO_ID"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
Requires Python 3.10+.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install .
|
|
22
|
+
# or for development:
|
|
23
|
+
pip install -e .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Configure
|
|
27
|
+
|
|
28
|
+
YouRead supports OpenAI and Gemini. Defaults:
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
llm:
|
|
32
|
+
provider: openai
|
|
33
|
+
model: gpt-5
|
|
34
|
+
max_tokens: 8000
|
|
35
|
+
temperature: 0.3
|
|
36
|
+
output:
|
|
37
|
+
directory: ./output
|
|
38
|
+
processing:
|
|
39
|
+
remove_sponsors: true
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# set provider + model
|
|
44
|
+
youread config set --provider openai --model gpt-5
|
|
45
|
+
# set API key (secure prompt, no echo)
|
|
46
|
+
youread config set --set-api-key
|
|
47
|
+
# view current config
|
|
48
|
+
youread config show
|
|
49
|
+
# check if active provider has a key
|
|
50
|
+
youread config check
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Provider/model pairs are validated at save time — `--provider gemini --model gpt-5` is rejected immediately.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
youread run "https://youtube.com/watch?v=VIDEO_ID"
|
|
59
|
+
|
|
60
|
+
# with options
|
|
61
|
+
youread run "URL" --mode detailed
|
|
62
|
+
youread run "URL" --prompt "Focus on code examples"
|
|
63
|
+
youread run "URL" --model gemini-2.5-flash
|
|
64
|
+
youread run "URL" --no-sponsorblock
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| Flag | What it does |
|
|
68
|
+
|------|-------------|
|
|
69
|
+
| `--mode summary\|standard\|detailed` | Article depth (default: standard) |
|
|
70
|
+
| `--prompt "..."` | Custom instructions appended to the system prompt |
|
|
71
|
+
| `--model <name>` | Override configured model for one run |
|
|
72
|
+
| `--no-sponsorblock` | Skip sponsor segment removal |
|
|
73
|
+
|
|
74
|
+
Output goes to the configured directory (`./output` by default). After generation, YouRead asks whether to open the file.
|
|
75
|
+
|
|
76
|
+
## Supported Providers
|
|
77
|
+
|
|
78
|
+
| Provider | Example Model | Env variable |
|
|
79
|
+
|----------|---------------|-------------|
|
|
80
|
+
| OpenAI | `gpt-5` | `OPENAI_API_KEY` |
|
|
81
|
+
| Gemini | `gemini-2.5-flash` | `GEMINI_API_KEY` |
|
|
82
|
+
|
|
83
|
+
## Limitations
|
|
84
|
+
|
|
85
|
+
YouRead is transcript-only. It doesn't see video frames, slides, or code on screen.
|
|
86
|
+
|
|
87
|
+
| Situation | Outcome |
|
|
88
|
+
|-----------|---------|
|
|
89
|
+
| No transcript | Can't process |
|
|
90
|
+
| Auto-generated captions | Quality suffers |
|
|
91
|
+
| Visual-heavy content | Details missed |
|
|
92
|
+
| Code not spoken | Won't appear |
|
|
93
|
+
| Non-English video | English captions only |
|
|
94
|
+
|
|
95
|
+
For best results, use videos where the speaker explains things verbally.
|
|
96
|
+
|
|
97
|
+
## Troubleshooting
|
|
98
|
+
|
|
99
|
+
**`No API key found`** — `youread config check` then `youread config set --set-api-key`.
|
|
100
|
+
|
|
101
|
+
**`Provider mismatch`** — You paired `--provider gemini` with `--model gpt-5`. Use matching pairs: `openai` + `gpt-5`, `gemini` + `gemini-2.5-flash`.
|
|
102
|
+
|
|
103
|
+
**`No transcript available`** — Video has no accessible captions.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "youread"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Convert YouTube videos into comprehensive, reading-optimized markdown articles"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Guransh Singh (singhDevs)"},
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Topic :: Utilities",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
dependencies = [
|
|
27
|
+
"youtube-transcript-api>=0.6.0",
|
|
28
|
+
"google-genai>=1.0.0",
|
|
29
|
+
"python-dotenv>=1.0.0",
|
|
30
|
+
"pyyaml>=6.0",
|
|
31
|
+
"typer>=0.9.0",
|
|
32
|
+
"rich>=13.0.0",
|
|
33
|
+
"openai==2.16.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/singhDevs/YouRead"
|
|
38
|
+
Issues = "https://github.com/singhDevs/YouRead/issues"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
youread = "youread.main:app"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["src"]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.package-data]
|
|
47
|
+
youread = ["config/settings.yaml", "prompts/*.md", "prompts/modes/*.md"]
|
youread-0.2.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Article generation using LLM providers."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Literal
|
|
7
|
+
from importlib.resources import files
|
|
8
|
+
|
|
9
|
+
from .models import VideoInfo
|
|
10
|
+
from .providers import call_llm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Valid mode choices
|
|
14
|
+
ArticleMode = Literal["summary", "standard", "detailed"]
|
|
15
|
+
|
|
16
|
+
# Reading speed for technical content (words per minute)
|
|
17
|
+
# 150 WPM is realistic for technical/educational material requiring comprehension
|
|
18
|
+
TECHNICAL_WPM = 150
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def calculate_reading_time(text: str, wpm: int = TECHNICAL_WPM) -> str:
|
|
22
|
+
"""Calculate reading time from text content.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
text: The article text to measure.
|
|
26
|
+
wpm: Words per minute reading speed.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Formatted reading time string (e.g., "~5 min read").
|
|
30
|
+
"""
|
|
31
|
+
# Count words (split on whitespace)
|
|
32
|
+
word_count = len(text.split())
|
|
33
|
+
|
|
34
|
+
# Calculate minutes, round up to nearest minute
|
|
35
|
+
minutes = math.ceil(word_count / wpm)
|
|
36
|
+
|
|
37
|
+
return f"~{minutes} min read"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def insert_reading_time(article: str) -> str:
|
|
41
|
+
"""Replace the reading time placeholder with calculated reading time.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
article: The generated article with READING_TIME_PLACEHOLDER.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Article with accurate reading time inserted.
|
|
48
|
+
"""
|
|
49
|
+
reading_time = calculate_reading_time(article)
|
|
50
|
+
return article.replace("READING_TIME_PLACEHOLDER", reading_time)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_prompt(prompt_path: Optional[Path] = None) -> str:
|
|
54
|
+
"""Load a prompt from file.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
prompt_path: Path to the prompt file. Defaults to prompts/system_prompt_v2.md.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The prompt content as a string.
|
|
61
|
+
"""
|
|
62
|
+
if prompt_path is None:
|
|
63
|
+
prompt_path = files("youread.prompts") / "system_prompt_v2.md"
|
|
64
|
+
|
|
65
|
+
if hasattr(prompt_path, 'read_text'):
|
|
66
|
+
return prompt_path.read_text(encoding="utf-8")
|
|
67
|
+
|
|
68
|
+
path = Path(prompt_path)
|
|
69
|
+
if not path.exists():
|
|
70
|
+
raise FileNotFoundError(f"Prompt file not found: {prompt_path}")
|
|
71
|
+
|
|
72
|
+
return path.read_text(encoding="utf-8")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def load_mode_prompt(mode: ArticleMode) -> str:
|
|
76
|
+
"""Load the mode-specific prompt fragment.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
mode: The article generation mode (summary, standard, detailed).
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The mode prompt content as a string.
|
|
83
|
+
"""
|
|
84
|
+
mode_path = files("youread.prompts.modes") / f"{mode}.md"
|
|
85
|
+
|
|
86
|
+
if hasattr(mode_path, 'read_text'):
|
|
87
|
+
return mode_path.read_text(encoding="utf-8")
|
|
88
|
+
|
|
89
|
+
path = Path(mode_path)
|
|
90
|
+
if not path.exists():
|
|
91
|
+
raise FileNotFoundError(f"Mode prompt file not found: {mode_path}")
|
|
92
|
+
|
|
93
|
+
return path.read_text(encoding="utf-8")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def compose_system_prompt(
|
|
97
|
+
mode: ArticleMode = "standard",
|
|
98
|
+
custom_prompt: Optional[str] = None,
|
|
99
|
+
video_id: Optional[str] = None,
|
|
100
|
+
) -> str:
|
|
101
|
+
"""Compose the full system prompt from base + mode + custom.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
mode: The article generation mode.
|
|
105
|
+
custom_prompt: Optional user-provided custom instructions.
|
|
106
|
+
video_id: Optional video ID for constructing the source URL.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The composed system prompt.
|
|
110
|
+
"""
|
|
111
|
+
# Load base prompt
|
|
112
|
+
base_prompt = load_prompt()
|
|
113
|
+
|
|
114
|
+
# Load mode-specific instructions
|
|
115
|
+
mode_instructions = load_mode_prompt(mode)
|
|
116
|
+
|
|
117
|
+
# Replace the mode placeholder
|
|
118
|
+
prompt = base_prompt.replace("{{MODE_INSTRUCTIONS}}", mode_instructions)
|
|
119
|
+
|
|
120
|
+
# Replace video URL placeholder if video_id provided
|
|
121
|
+
if video_id:
|
|
122
|
+
video_url = f"https://youtube.com/watch?v={video_id}"
|
|
123
|
+
prompt = prompt.replace("VIDEO_URL_PLACEHOLDER", video_url)
|
|
124
|
+
|
|
125
|
+
# Append custom prompt if provided
|
|
126
|
+
if custom_prompt:
|
|
127
|
+
custom_section = f"""
|
|
128
|
+
<custom_instructions>
|
|
129
|
+
The user has provided the following additional instructions. Follow them while maintaining the core quality standards:
|
|
130
|
+
|
|
131
|
+
{custom_prompt}
|
|
132
|
+
</custom_instructions>
|
|
133
|
+
"""
|
|
134
|
+
prompt += custom_section
|
|
135
|
+
|
|
136
|
+
return prompt
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def generate_article(
|
|
140
|
+
transcript_text: str,
|
|
141
|
+
video_info: VideoInfo,
|
|
142
|
+
provider: str,
|
|
143
|
+
api_key: str,
|
|
144
|
+
video_id: Optional[str] = None,
|
|
145
|
+
model: str = "gpt-5",
|
|
146
|
+
max_tokens: int = 8000,
|
|
147
|
+
temperature: float = 0.3,
|
|
148
|
+
mode: ArticleMode = "standard",
|
|
149
|
+
custom_prompt: Optional[str] = None,
|
|
150
|
+
) -> str:
|
|
151
|
+
"""Generate a readable article from a video transcript.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
transcript_text: The full transcript text.
|
|
155
|
+
video_info: Video metadata (title, channel).
|
|
156
|
+
provider: LLM provider name (e.g., "openai", "gemini").
|
|
157
|
+
api_key: API key for the provider.
|
|
158
|
+
video_id: Video ID for source link.
|
|
159
|
+
model: Model identifier to use.
|
|
160
|
+
max_tokens: Maximum tokens in the response.
|
|
161
|
+
temperature: Sampling temperature (0.0-1.0).
|
|
162
|
+
mode: Article generation mode (summary, standard, detailed).
|
|
163
|
+
custom_prompt: Optional custom instructions from user.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The generated article as markdown text.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
Exception: If the API call fails.
|
|
170
|
+
"""
|
|
171
|
+
# Compose the system prompt
|
|
172
|
+
system_prompt = compose_system_prompt(
|
|
173
|
+
mode=mode,
|
|
174
|
+
custom_prompt=custom_prompt,
|
|
175
|
+
video_id=video_id,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Build the user prompt with context
|
|
179
|
+
user_prompt = f"""## Video Information
|
|
180
|
+
**Title:** {video_info.title}
|
|
181
|
+
**Channel:** {video_info.channel}
|
|
182
|
+
|
|
183
|
+
## Transcript
|
|
184
|
+
{transcript_text}
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
# Generate the article via provider dispatch
|
|
188
|
+
raw_response = call_llm(
|
|
189
|
+
provider=provider,
|
|
190
|
+
api_key=api_key,
|
|
191
|
+
model=model,
|
|
192
|
+
system_prompt=system_prompt,
|
|
193
|
+
user_prompt=user_prompt,
|
|
194
|
+
max_tokens=max_tokens,
|
|
195
|
+
temperature=temperature,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Calculate and insert accurate reading time
|
|
199
|
+
article = insert_reading_time(raw_response)
|
|
200
|
+
|
|
201
|
+
return article
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Custom exceptions for YouRead."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class YouReadError(Exception):
|
|
5
|
+
"""Base exception for all YouRead errors."""
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InvalidURLError(YouReadError):
|
|
10
|
+
def __init__(self, url: str) -> None:
|
|
11
|
+
self.url = url
|
|
12
|
+
super().__init__(f"Invalid YouTube URL: {url}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TranscriptNotFoundError(YouReadError):
|
|
16
|
+
def __init__(self, video_id: str) -> None:
|
|
17
|
+
self.video_id = video_id
|
|
18
|
+
super().__init__(f"No transcript available for video: {video_id}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VideoNotFoundError(YouReadError):
|
|
22
|
+
def __init__(self, video_id: str) -> None:
|
|
23
|
+
self.video_id = video_id
|
|
24
|
+
super().__init__(f"Video not found or unavailable: {video_id}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UnsupportedProviderError(YouReadError):
|
|
28
|
+
def __init__(self, provider: str, supported: list[str]) -> None:
|
|
29
|
+
self.provider = provider
|
|
30
|
+
self.supported = supported
|
|
31
|
+
super().__init__(
|
|
32
|
+
f"Unsupported provider '{provider}'. "
|
|
33
|
+
f"Supported: {', '.join(supported)}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class APIKeyMissingError(YouReadError):
|
|
38
|
+
def __init__(self, provider: str) -> None:
|
|
39
|
+
self.provider = provider
|
|
40
|
+
env_var = f"{provider.upper()}_API_KEY"
|
|
41
|
+
super().__init__(
|
|
42
|
+
f"API key for '{provider}' not found. "
|
|
43
|
+
f"Save {env_var} to YouRead's managed .env file, or use: "
|
|
44
|
+
f"youread config set --provider {provider} --set-api-key"
|
|
45
|
+
)
|