@heylemon/lemonade 0.2.4 → 0.2.5
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/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/package.json +1 -1
- package/skills/apple-notes/SKILL.md +0 -50
- package/skills/apple-reminders/SKILL.md +0 -67
- package/skills/goplaces/SKILL.md +0 -30
- package/skills/local-places/SERVER_README.md +0 -101
- package/skills/local-places/SKILL.md +0 -91
- package/skills/local-places/pyproject.toml +0 -27
- package/skills/local-places/src/local_places/__init__.py +0 -2
- package/skills/local-places/src/local_places/google_places.py +0 -314
- package/skills/local-places/src/local_places/main.py +0 -65
- package/skills/local-places/src/local_places/schemas.py +0 -107
- package/skills/messages/SKILL.md +0 -125
- package/skills/openai-image-gen/SKILL.md +0 -71
- package/skills/openai-image-gen/scripts/gen.py +0 -255
- package/skills/ordercli/SKILL.md +0 -47
- package/skills/spotify-player/SKILL.md +0 -38
- package/skills/youtube-watcher/SKILL.md +0 -51
- package/skills/youtube-watcher/scripts/get_transcript.py +0 -81
- /package/skills/eightctl/{SKILL.md → SKILL.md.disabled} +0 -0
- /package/skills/nano-banana-pro/{SKILL.md → SKILL.md.disabled} +0 -0
- /package/skills/openai-whisper-api/{SKILL.md → SKILL.md.disabled} +0 -0
- /package/skills/openhue/{SKILL.md → SKILL.md.disabled} +0 -0
- /package/skills/sag/{SKILL.md → SKILL.md.disabled} +0 -0
- /package/skills/sherpa-onnx-tts/{SKILL.md → SKILL.md.disabled} +0 -0
- /package/skills/sonoscli/{SKILL.md → SKILL.md.disabled} +0 -0
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import argparse
|
|
3
|
-
import base64
|
|
4
|
-
import datetime as dt
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import random
|
|
8
|
-
import re
|
|
9
|
-
import sys
|
|
10
|
-
import urllib.error
|
|
11
|
-
import urllib.request
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def slugify(text: str) -> str:
|
|
16
|
-
text = text.lower().strip()
|
|
17
|
-
text = re.sub(r"[^a-z0-9]+", "-", text)
|
|
18
|
-
text = re.sub(r"-{2,}", "-", text).strip("-")
|
|
19
|
-
return text or "image"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def default_out_dir() -> Path:
|
|
23
|
-
now = dt.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
|
|
24
|
-
preferred = Path.home() / "Projects" / "tmp"
|
|
25
|
-
base = preferred if preferred.is_dir() else Path("./tmp")
|
|
26
|
-
base.mkdir(parents=True, exist_ok=True)
|
|
27
|
-
return base / f"openai-image-gen-{now}"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def pick_prompts(count: int) -> list[str]:
|
|
31
|
-
subjects = [
|
|
32
|
-
"a lobster astronaut",
|
|
33
|
-
"a brutalist lighthouse",
|
|
34
|
-
"a cozy reading nook",
|
|
35
|
-
"a cyberpunk noodle shop",
|
|
36
|
-
"a Vienna street at dusk",
|
|
37
|
-
"a minimalist product photo",
|
|
38
|
-
"a surreal underwater library",
|
|
39
|
-
]
|
|
40
|
-
styles = [
|
|
41
|
-
"ultra-detailed studio photo",
|
|
42
|
-
"35mm film still",
|
|
43
|
-
"isometric illustration",
|
|
44
|
-
"editorial photography",
|
|
45
|
-
"soft watercolor",
|
|
46
|
-
"architectural render",
|
|
47
|
-
"high-contrast monochrome",
|
|
48
|
-
]
|
|
49
|
-
lighting = [
|
|
50
|
-
"golden hour",
|
|
51
|
-
"overcast soft light",
|
|
52
|
-
"neon lighting",
|
|
53
|
-
"dramatic rim light",
|
|
54
|
-
"candlelight",
|
|
55
|
-
"foggy atmosphere",
|
|
56
|
-
]
|
|
57
|
-
prompts: list[str] = []
|
|
58
|
-
for _ in range(count):
|
|
59
|
-
prompts.append(
|
|
60
|
-
f"{random.choice(styles)} of {random.choice(subjects)}, {random.choice(lighting)}"
|
|
61
|
-
)
|
|
62
|
-
return prompts
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def get_model_defaults(model: str) -> tuple[str, str]:
|
|
66
|
-
"""Return (default_size, default_quality) for the given model."""
|
|
67
|
-
if model == "dall-e-2":
|
|
68
|
-
# quality will be ignored
|
|
69
|
-
return ("1024x1024", "standard")
|
|
70
|
-
elif model == "dall-e-3":
|
|
71
|
-
return ("1024x1024", "standard")
|
|
72
|
-
else:
|
|
73
|
-
# GPT image or future models
|
|
74
|
-
return ("1024x1024", "high")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def resolve_api_url_and_key() -> tuple[str, str]:
|
|
78
|
-
"""Resolve the images API base URL and auth token.
|
|
79
|
-
Prefers the Lemonade backend proxy; falls back to direct OpenAI key."""
|
|
80
|
-
backend = (os.environ.get("LEMON_BACKEND_URL") or "").strip()
|
|
81
|
-
gw_token = (os.environ.get("GATEWAY_TOKEN") or "").strip()
|
|
82
|
-
if backend and gw_token:
|
|
83
|
-
return f"{backend}/api/lemonade/proxy/v1/images/generations", gw_token
|
|
84
|
-
api_key = (os.environ.get("OPENAI_API_KEY") or "").strip()
|
|
85
|
-
if api_key:
|
|
86
|
-
return "https://api.openai.com/v1/images/generations", api_key
|
|
87
|
-
return "", ""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def request_images(
|
|
91
|
-
api_key: str,
|
|
92
|
-
prompt: str,
|
|
93
|
-
model: str,
|
|
94
|
-
size: str,
|
|
95
|
-
quality: str,
|
|
96
|
-
background: str = "",
|
|
97
|
-
output_format: str = "",
|
|
98
|
-
style: str = "",
|
|
99
|
-
api_url: str = "",
|
|
100
|
-
) -> dict:
|
|
101
|
-
url = api_url or "https://api.openai.com/v1/images/generations"
|
|
102
|
-
args = {
|
|
103
|
-
"model": model,
|
|
104
|
-
"prompt": prompt,
|
|
105
|
-
"size": size,
|
|
106
|
-
"n": 1,
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
# Quality parameter - dall-e-2 doesn't accept this parameter
|
|
110
|
-
if model != "dall-e-2":
|
|
111
|
-
args["quality"] = quality
|
|
112
|
-
|
|
113
|
-
# Note: response_format no longer supported by OpenAI Images API
|
|
114
|
-
# dall-e models now return URLs by default
|
|
115
|
-
|
|
116
|
-
if model.startswith("gpt-image"):
|
|
117
|
-
if background:
|
|
118
|
-
args["background"] = background
|
|
119
|
-
if output_format:
|
|
120
|
-
args["output_format"] = output_format
|
|
121
|
-
|
|
122
|
-
if model == "dall-e-3" and style:
|
|
123
|
-
args["style"] = style
|
|
124
|
-
|
|
125
|
-
body = json.dumps(args).encode("utf-8")
|
|
126
|
-
req = urllib.request.Request(
|
|
127
|
-
url,
|
|
128
|
-
method="POST",
|
|
129
|
-
headers={
|
|
130
|
-
"Authorization": f"Bearer {api_key}",
|
|
131
|
-
"Content-Type": "application/json",
|
|
132
|
-
},
|
|
133
|
-
data=body,
|
|
134
|
-
)
|
|
135
|
-
try:
|
|
136
|
-
with urllib.request.urlopen(req, timeout=300) as resp:
|
|
137
|
-
return json.loads(resp.read().decode("utf-8"))
|
|
138
|
-
except urllib.error.HTTPError as e:
|
|
139
|
-
payload = e.read().decode("utf-8", errors="replace")
|
|
140
|
-
raise RuntimeError(f"OpenAI Images API failed ({e.code}): {payload}") from e
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def write_gallery(out_dir: Path, items: list[dict]) -> None:
|
|
144
|
-
thumbs = "\n".join(
|
|
145
|
-
[
|
|
146
|
-
f"""
|
|
147
|
-
<figure>
|
|
148
|
-
<a href="{it["file"]}"><img src="{it["file"]}" loading="lazy" /></a>
|
|
149
|
-
<figcaption>{it["prompt"]}</figcaption>
|
|
150
|
-
</figure>
|
|
151
|
-
""".strip()
|
|
152
|
-
for it in items
|
|
153
|
-
]
|
|
154
|
-
)
|
|
155
|
-
html = f"""<!doctype html>
|
|
156
|
-
<meta charset="utf-8" />
|
|
157
|
-
<title>openai-image-gen</title>
|
|
158
|
-
<style>
|
|
159
|
-
:root {{ color-scheme: dark; }}
|
|
160
|
-
body {{ margin: 24px; font: 14px/1.4 ui-sans-serif, system-ui; background: #0b0f14; color: #e8edf2; }}
|
|
161
|
-
h1 {{ font-size: 18px; margin: 0 0 16px; }}
|
|
162
|
-
.grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }}
|
|
163
|
-
figure {{ margin: 0; padding: 12px; border: 1px solid #1e2a36; border-radius: 14px; background: #0f1620; }}
|
|
164
|
-
img {{ width: 100%; height: auto; border-radius: 10px; display: block; }}
|
|
165
|
-
figcaption {{ margin-top: 10px; color: #b7c2cc; }}
|
|
166
|
-
code {{ color: #9cd1ff; }}
|
|
167
|
-
</style>
|
|
168
|
-
<h1>openai-image-gen</h1>
|
|
169
|
-
<p>Output: <code>{out_dir.as_posix()}</code></p>
|
|
170
|
-
<div class="grid">
|
|
171
|
-
{thumbs}
|
|
172
|
-
</div>
|
|
173
|
-
"""
|
|
174
|
-
(out_dir / "index.html").write_text(html, encoding="utf-8")
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def main() -> int:
|
|
178
|
-
ap = argparse.ArgumentParser(description="Generate images via OpenAI Images API.")
|
|
179
|
-
ap.add_argument("--prompt", help="Single prompt. If omitted, random prompts are generated.")
|
|
180
|
-
ap.add_argument("--count", type=int, default=8, help="How many images to generate.")
|
|
181
|
-
ap.add_argument("--model", default="gpt-image-1", help="Image model id.")
|
|
182
|
-
ap.add_argument("--size", default="", help="Image size (e.g. 1024x1024, 1536x1024). Defaults based on model if not specified.")
|
|
183
|
-
ap.add_argument("--quality", default="", help="Image quality (e.g. high, standard). Defaults based on model if not specified.")
|
|
184
|
-
ap.add_argument("--background", default="", help="Background transparency (GPT models only): transparent, opaque, or auto.")
|
|
185
|
-
ap.add_argument("--output-format", default="", help="Output format (GPT models only): png, jpeg, or webp.")
|
|
186
|
-
ap.add_argument("--style", default="", help="Image style (dall-e-3 only): vivid or natural.")
|
|
187
|
-
ap.add_argument("--out-dir", default="", help="Output directory (default: ./tmp/openai-image-gen-<ts>).")
|
|
188
|
-
args = ap.parse_args()
|
|
189
|
-
|
|
190
|
-
api_url, api_key = resolve_api_url_and_key()
|
|
191
|
-
if not api_key:
|
|
192
|
-
print("No API credentials available for image generation", file=sys.stderr)
|
|
193
|
-
return 2
|
|
194
|
-
|
|
195
|
-
# Apply model-specific defaults if not specified
|
|
196
|
-
default_size, default_quality = get_model_defaults(args.model)
|
|
197
|
-
size = args.size or default_size
|
|
198
|
-
quality = args.quality or default_quality
|
|
199
|
-
|
|
200
|
-
count = args.count
|
|
201
|
-
if args.model == "dall-e-3" and count > 1:
|
|
202
|
-
print(f"Warning: dall-e-3 only supports generating 1 image at a time. Reducing count from {count} to 1.", file=sys.stderr)
|
|
203
|
-
count = 1
|
|
204
|
-
|
|
205
|
-
out_dir = Path(args.out_dir).expanduser() if args.out_dir else default_out_dir()
|
|
206
|
-
out_dir.mkdir(parents=True, exist_ok=True)
|
|
207
|
-
|
|
208
|
-
prompts = [args.prompt] * count if args.prompt else pick_prompts(count)
|
|
209
|
-
|
|
210
|
-
# Determine file extension based on output format
|
|
211
|
-
if args.model.startswith("gpt-image") and args.output_format:
|
|
212
|
-
file_ext = args.output_format
|
|
213
|
-
else:
|
|
214
|
-
file_ext = "png"
|
|
215
|
-
|
|
216
|
-
items: list[dict] = []
|
|
217
|
-
for idx, prompt in enumerate(prompts, start=1):
|
|
218
|
-
print(f"[{idx}/{len(prompts)}] {prompt}")
|
|
219
|
-
res = request_images(
|
|
220
|
-
api_key,
|
|
221
|
-
prompt,
|
|
222
|
-
args.model,
|
|
223
|
-
size,
|
|
224
|
-
quality,
|
|
225
|
-
args.background,
|
|
226
|
-
args.output_format,
|
|
227
|
-
args.style,
|
|
228
|
-
api_url=api_url,
|
|
229
|
-
)
|
|
230
|
-
data = res.get("data", [{}])[0]
|
|
231
|
-
image_b64 = data.get("b64_json")
|
|
232
|
-
image_url = data.get("url")
|
|
233
|
-
if not image_b64 and not image_url:
|
|
234
|
-
raise RuntimeError(f"Unexpected response: {json.dumps(res)[:400]}")
|
|
235
|
-
|
|
236
|
-
filename = f"{idx:03d}-{slugify(prompt)[:40]}.{file_ext}"
|
|
237
|
-
filepath = out_dir / filename
|
|
238
|
-
if image_b64:
|
|
239
|
-
filepath.write_bytes(base64.b64decode(image_b64))
|
|
240
|
-
else:
|
|
241
|
-
try:
|
|
242
|
-
urllib.request.urlretrieve(image_url, filepath)
|
|
243
|
-
except urllib.error.URLError as e:
|
|
244
|
-
raise RuntimeError(f"Failed to download image from {image_url}: {e}") from e
|
|
245
|
-
|
|
246
|
-
items.append({"prompt": prompt, "file": filename})
|
|
247
|
-
|
|
248
|
-
(out_dir / "prompts.json").write_text(json.dumps(items, indent=2), encoding="utf-8")
|
|
249
|
-
write_gallery(out_dir, items)
|
|
250
|
-
print(f"\nWrote: {(out_dir / 'index.html').as_posix()}")
|
|
251
|
-
return 0
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if __name__ == "__main__":
|
|
255
|
-
raise SystemExit(main())
|
package/skills/ordercli/SKILL.md
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: ordercli
|
|
3
|
-
description: Foodora-only CLI for checking past orders and active order status (Deliveroo WIP).
|
|
4
|
-
homepage: https://ordercli.sh
|
|
5
|
-
metadata: {"lemonade":{"emoji":"🛵","requires":{"bins":["ordercli"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/ordercli","bins":["ordercli"],"label":"Install ordercli (brew)"},{"id":"go","kind":"go","module":"github.com/steipete/ordercli/cmd/ordercli@latest","bins":["ordercli"],"label":"Install ordercli (go)"}]}}
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# ordercli
|
|
9
|
-
|
|
10
|
-
Use `ordercli` to check past orders and track active order status (Foodora only right now).
|
|
11
|
-
|
|
12
|
-
Quick start (Foodora)
|
|
13
|
-
- `ordercli foodora countries`
|
|
14
|
-
- `ordercli foodora config set --country AT`
|
|
15
|
-
- `ordercli foodora login --email you@example.com --password-stdin`
|
|
16
|
-
- `ordercli foodora orders`
|
|
17
|
-
- `ordercli foodora history --limit 20`
|
|
18
|
-
- `ordercli foodora history show <orderCode>`
|
|
19
|
-
|
|
20
|
-
Orders
|
|
21
|
-
- Active list (arrival/status): `ordercli foodora orders`
|
|
22
|
-
- Watch: `ordercli foodora orders --watch`
|
|
23
|
-
- Active order detail: `ordercli foodora order <orderCode>`
|
|
24
|
-
- History detail JSON: `ordercli foodora history show <orderCode> --json`
|
|
25
|
-
|
|
26
|
-
Reorder (adds to cart)
|
|
27
|
-
- Preview: `ordercli foodora reorder <orderCode>`
|
|
28
|
-
- Confirm: `ordercli foodora reorder <orderCode> --confirm`
|
|
29
|
-
- Address: `ordercli foodora reorder <orderCode> --confirm --address-id <id>`
|
|
30
|
-
|
|
31
|
-
Cloudflare / bot protection
|
|
32
|
-
- Browser login: `ordercli foodora login --email you@example.com --password-stdin --browser`
|
|
33
|
-
- Reuse profile: `--browser-profile "$HOME/Library/Application Support/ordercli/browser-profile"`
|
|
34
|
-
- Import Chrome cookies: `ordercli foodora cookies chrome --profile "Default"`
|
|
35
|
-
|
|
36
|
-
Session import (no password)
|
|
37
|
-
- `ordercli foodora session chrome --url https://www.foodora.at/ --profile "Default"`
|
|
38
|
-
- `ordercli foodora session refresh --client-id android`
|
|
39
|
-
|
|
40
|
-
Deliveroo (WIP, not working yet)
|
|
41
|
-
- Requires `DELIVEROO_BEARER_TOKEN` (optional `DELIVEROO_COOKIE`).
|
|
42
|
-
- `ordercli deliveroo config set --market uk`
|
|
43
|
-
- `ordercli deliveroo history`
|
|
44
|
-
|
|
45
|
-
Notes
|
|
46
|
-
- Use `--config /tmp/ordercli.json` for testing.
|
|
47
|
-
- Confirm before any reorder or cart-changing action.
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: spotify-player
|
|
3
|
-
description: Terminal Spotify playback/search via spogo (preferred) or spotify_player.
|
|
4
|
-
homepage: https://www.spotify.com
|
|
5
|
-
metadata: {"lemonade":{"emoji":"🎵","requires":{"anyBins":["spogo","spotify_player"]},"install":[{"id":"brew","kind":"brew","formula":"spogo","tap":"steipete/tap","bins":["spogo"],"label":"Install spogo (brew)"},{"id":"brew","kind":"brew","formula":"spotify_player","bins":["spotify_player"],"label":"Install spotify_player (brew)"}]}}
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# spogo / spotify_player (SECONDARY — prefer AppleScript)
|
|
9
|
-
|
|
10
|
-
**Default method: AppleScript** (see spotify skill). Use AppleScript to control Spotify unless the user specifically asks for CLI-based control or AppleScript is unavailable.
|
|
11
|
-
|
|
12
|
-
This CLI skill is an **alternative** for users who prefer terminal-based Spotify control.
|
|
13
|
-
|
|
14
|
-
**Never use the browser for Spotify.** Do not open open.spotify.com or use browser tools for playback.
|
|
15
|
-
|
|
16
|
-
Requirements
|
|
17
|
-
- Spotify Premium account.
|
|
18
|
-
- Either `spogo` or `spotify_player` installed.
|
|
19
|
-
|
|
20
|
-
spogo setup
|
|
21
|
-
- Import cookies: `spogo auth import --browser chrome`
|
|
22
|
-
|
|
23
|
-
Common CLI commands
|
|
24
|
-
- Search: `spogo search track "query"`
|
|
25
|
-
- Playback: `spogo play|pause|next|prev`
|
|
26
|
-
- Devices: `spogo device list`, `spogo device set "<name|id>"`
|
|
27
|
-
- Status: `spogo status`
|
|
28
|
-
|
|
29
|
-
spotify_player commands (fallback)
|
|
30
|
-
- Search: `spotify_player search "query"`
|
|
31
|
-
- Playback: `spotify_player playback play|pause|next|previous`
|
|
32
|
-
- Connect device: `spotify_player connect`
|
|
33
|
-
- Like track: `spotify_player like`
|
|
34
|
-
|
|
35
|
-
Notes
|
|
36
|
-
- Config folder: `~/.config/spotify-player` (e.g., `app.toml`).
|
|
37
|
-
- For Spotify Connect integration, set a user `client_id` in config.
|
|
38
|
-
- TUI shortcuts are available via `?` in the app.
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: youtube-watcher
|
|
3
|
-
description: Fetch and read transcripts from YouTube videos. Use when you need to summarize a video, answer questions about its content, or extract information from it.
|
|
4
|
-
author: michael gathara
|
|
5
|
-
version: 1.0.0
|
|
6
|
-
triggers:
|
|
7
|
-
- "watch youtube"
|
|
8
|
-
- "summarize video"
|
|
9
|
-
- "video transcript"
|
|
10
|
-
- "youtube summary"
|
|
11
|
-
- "analyze video"
|
|
12
|
-
metadata: {"lemonade":{"emoji":"📺","requires":{"bins":["yt-dlp"]},"install":[{"id":"brew","kind":"brew","formula":"yt-dlp","bins":["yt-dlp"],"label":"Install yt-dlp (brew)"},{"id":"pip","kind":"pip","package":"yt-dlp","bins":["yt-dlp"],"label":"Install yt-dlp (pip)"}]}}
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
# YouTube Watcher
|
|
16
|
-
|
|
17
|
-
Fetch transcripts from YouTube videos to enable summarization, QA, and content extraction.
|
|
18
|
-
|
|
19
|
-
## Usage
|
|
20
|
-
|
|
21
|
-
### Get Transcript
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
python3 {baseDir}/scripts/get_transcript.py "https://www.youtube.com/watch?v=VIDEO_ID"
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Examples
|
|
28
|
-
|
|
29
|
-
**Summarize a video:**
|
|
30
|
-
|
|
31
|
-
1. Get the transcript:
|
|
32
|
-
```bash
|
|
33
|
-
python3 {baseDir}/scripts/get_transcript.py "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
|
34
|
-
```
|
|
35
|
-
2. Read the output and summarize it for the user.
|
|
36
|
-
|
|
37
|
-
**Find specific information:**
|
|
38
|
-
|
|
39
|
-
1. Get the transcript.
|
|
40
|
-
2. Search the text for keywords or answer the user's question based on the content.
|
|
41
|
-
|
|
42
|
-
## Important
|
|
43
|
-
|
|
44
|
-
**If `lemon-youtube` is available**, prefer using `lemon-youtube transcript <url>` instead — it uses the authenticated YouTube API and is more reliable. Use this skill as a fallback when the YouTube integration is not connected or `lemon-youtube` is unavailable.
|
|
45
|
-
|
|
46
|
-
## Notes
|
|
47
|
-
|
|
48
|
-
- Requires `yt-dlp` to be installed and available in the PATH.
|
|
49
|
-
- Works with videos that have closed captions (CC) or auto-generated subtitles.
|
|
50
|
-
- If a video has no subtitles, the script will fail with an error message.
|
|
51
|
-
- No API key required — uses yt-dlp to fetch publicly available subtitles.
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import argparse
|
|
3
|
-
import os
|
|
4
|
-
import re
|
|
5
|
-
import subprocess
|
|
6
|
-
import sys
|
|
7
|
-
import tempfile
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
def clean_vtt(content: str) -> str:
|
|
11
|
-
"""
|
|
12
|
-
Clean WebVTT content to plain text.
|
|
13
|
-
Removes headers, timestamps, and duplicate lines.
|
|
14
|
-
"""
|
|
15
|
-
lines = content.splitlines()
|
|
16
|
-
text_lines = []
|
|
17
|
-
seen = set()
|
|
18
|
-
|
|
19
|
-
timestamp_pattern = re.compile(r'\d{2}:\d{2}:\d{2}\.\d{3}\s-->\s\d{2}:\d{2}:\d{2}\.\d{3}')
|
|
20
|
-
|
|
21
|
-
for line in lines:
|
|
22
|
-
line = line.strip()
|
|
23
|
-
if not line or line == 'WEBVTT' or line.isdigit():
|
|
24
|
-
continue
|
|
25
|
-
if timestamp_pattern.match(line):
|
|
26
|
-
continue
|
|
27
|
-
if line.startswith('NOTE') or line.startswith('STYLE'):
|
|
28
|
-
continue
|
|
29
|
-
|
|
30
|
-
if text_lines and text_lines[-1] == line:
|
|
31
|
-
continue
|
|
32
|
-
|
|
33
|
-
line = re.sub(r'<[^>]+>', '', line)
|
|
34
|
-
|
|
35
|
-
text_lines.append(line)
|
|
36
|
-
|
|
37
|
-
return '\n'.join(text_lines)
|
|
38
|
-
|
|
39
|
-
def get_transcript(url: str):
|
|
40
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
41
|
-
cmd = [
|
|
42
|
-
"yt-dlp",
|
|
43
|
-
"--write-subs",
|
|
44
|
-
"--write-auto-subs",
|
|
45
|
-
"--skip-download",
|
|
46
|
-
"--sub-lang", "en",
|
|
47
|
-
"--output", "subs",
|
|
48
|
-
url
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
try:
|
|
52
|
-
subprocess.run(cmd, cwd=temp_dir, check=True, capture_output=True)
|
|
53
|
-
except subprocess.CalledProcessError as e:
|
|
54
|
-
print(f"Error running yt-dlp: {e.stderr.decode()}", file=sys.stderr)
|
|
55
|
-
sys.exit(1)
|
|
56
|
-
except FileNotFoundError:
|
|
57
|
-
print("Error: yt-dlp not found. Please install it.", file=sys.stderr)
|
|
58
|
-
sys.exit(1)
|
|
59
|
-
|
|
60
|
-
temp_path = Path(temp_dir)
|
|
61
|
-
vtt_files = list(temp_path.glob("*.vtt"))
|
|
62
|
-
|
|
63
|
-
if not vtt_files:
|
|
64
|
-
print("No subtitles found.", file=sys.stderr)
|
|
65
|
-
sys.exit(1)
|
|
66
|
-
|
|
67
|
-
vtt_file = vtt_files[0]
|
|
68
|
-
|
|
69
|
-
content = vtt_file.read_text(encoding='utf-8')
|
|
70
|
-
clean_text = clean_vtt(content)
|
|
71
|
-
print(clean_text)
|
|
72
|
-
|
|
73
|
-
def main():
|
|
74
|
-
parser = argparse.ArgumentParser(description="Fetch YouTube transcript.")
|
|
75
|
-
parser.add_argument("url", help="YouTube video URL")
|
|
76
|
-
args = parser.parse_args()
|
|
77
|
-
|
|
78
|
-
get_transcript(args.url)
|
|
79
|
-
|
|
80
|
-
if __name__ == "__main__":
|
|
81
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|