Arshit Malik commited on
Commit
5478e7a
·
1 Parent(s): b9ba589

fix: remove secrets and token from repo, add gitignore

Browse files
Files changed (4) hide show
  1. .gitignore +10 -0
  2. TRANSFER_FILE.md +0 -1283
  3. client_secrets.json +0 -1
  4. token.pickle +0 -0
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ client_secrets.json
2
+ token.pickle
3
+ *.pickle
4
+ __pycache__/
5
+ *.pyc
6
+ runs/
7
+ pipeline.log
8
+ trigger_run
9
+ trigger_skip
10
+ TRANSFER_FILE.md
TRANSFER_FILE.md DELETED
@@ -1,1283 +0,0 @@
1
- # YOUTUBE SHORTS PIPELINE — TRANSFER FILE
2
- **Updated:** 2026-04-25T21:31:32.117518
3
-
4
-
5
- ## COMPLETE CHAT HISTORY SUMMARY
6
-
7
- ### Session 1 — Hardware Proposal Verification
8
- - Verified: DeepSeek-R1-Distill-Qwen-32B IQ3_XS (~13.7GB) on Kaggle P100 (15.9GB VRAM)
9
- - flash_attn=False (P100 = sm_60, Flash Attention needs sm_70+) — MANDATORY
10
- - offload_kqv=False: pushes KV cache to RAM (frees VRAM)
11
- - n_gpu_layers=-1: all 64 layers to VRAM
12
- - type_k/type_v=q4_0: compressed KV cache
13
- - VRAM: 13.7 + 1.1 buffers = 14.8/15.9 GB — safe
14
- - RAM: 8.6 GB KV cache at 128k ctx — fits in 13 GB RAM
15
- - Speed: 3-5 tok/s at small ctx, <1 tok/s at full 128k
16
-
17
- ### Session 2 — Initial Pipeline Build (yt-shorts-bot-v2)
18
- - User: Arshit (arshitmalik on HF + Kaggle), MacBook Air M1, Delhi India
19
- - HF key: hf_REDACTED_SET_IN_HF_SECRETS
20
- - Kaggle: arshitmalik / REDACTED_SET_IN_KAGGLE_KEYS_JSON
21
- - Problem: OpenRouter + Groq rate limits, org bans
22
- - Built yt-shorts-bot-v2 with Kaggle DeepSeek-R1 for content + Kaggle FLUX images + Kaggle StyleTTS2 TTS
23
- - CONFIRMED WORKING: kaggle_template.py (FLUX images), kaggle_tts_stt_template.py (StyleTTS2+Whisper)
24
- - kaggle_template.py uses: {{PROMPTS_PLACEHOLDER}} and {{HF_TOKEN_PLACEHOLDER}}
25
- - kaggle_tts_stt_template.py uses: {{SCRIPT_PLACEHOLDER}} and {{VIDEO_SPEED_PLACEHOLDER}}
26
- - Output: frame_0.png through frame_9.png, voice.wav, word_timings.json
27
-
28
- ### Session 3 — Mac Setup Fixes
29
- - Bug: huggingface_hub not installed on Mac → pip3 install huggingface_hub
30
- - Bug: git auth format wrong → use https://arshitmalik:TOKEN@huggingface.co/... format
31
-
32
- ### Session 4 — Kaggle Kernel Permission Fix
33
- - Error: "cannot access kernel" permission denied
34
- - Root cause: Kaggle API pushes kernels PRIVATE by default
35
- - Fix: ALL kernel-metadata.json MUST have "is_private": "false" (string, not bool)
36
- - Also fixed: TTS slug had no random suffix (409 conflict on retry)
37
- - Also fixed: all slugs now include random suffix to avoid collisions
38
-
39
- ### Session 5 — HF Inference API proposal (REJECTED)
40
- - Proposed HF Inference API free tier — user rejected: only $0.10/month and 3 req/hour
41
-
42
- ### Session 6 — CURRENT: OpenClaw + Ollama + Kaggle Pipeline (yt-openclaw)
43
- - OpenClaw with local Ollama model (qwen2.5-coder:7b-instruct) as monitoring brain
44
- - Content generation stays on Kaggle (DeepSeek-R1-32B, cached Dataset)
45
- - Kaggle FLUX + StyleTTS2 UNCHANGED
46
- - OpenClaw communicates via Telegram bot
47
- - SOUL.md defines monitoring/control commands
48
- - HF Dataset (arshitmalik/yt-pipeline-data) for persistence
49
- - User already created Kaggle Dataset: deepseek-r1-32b-gguf (private, in arshitmalik account)
50
-
51
-
52
- ## CRITICAL CONTEXT FOR NEXT AI
53
-
54
- ### User Info
55
- - Name: Arshit | HF username: arshitmalik | Kaggle: arshitmalik
56
- - Mac: MacBook Air M1 (8GB RAM) — setup only, not 24/7
57
- - Location: Delhi, India — no credit card for any paid services
58
- - Goal: 100% free 24/7 YouTube Shorts science channel automation
59
-
60
- ### Current HF Space
61
- - Name: yt-openclaw (Docker, free: 2 vCPU, 16 GB RAM)
62
- - URL: https://arshitmalik-yt-openclaw.hf.space
63
- - Password: arshit2025 (OPENCLAW_PASSWORD secret)
64
-
65
- ### HF Dataset (persistence)
66
- - Name: arshitmalik/yt-pipeline-data (private)
67
- - Synced files: topic_history.json, fact_history.json, token.pickle
68
-
69
- ### Kaggle Dataset (GGUF model cache)
70
- - Name: deepseek-r1-32b-gguf (private, in arshitmalik account)
71
- - Contains: DeepSeek-R1-Distill-Qwen-32B-IQ3_XS.gguf
72
- - Mounted at: /kaggle/input/deepseek-r1-32b-gguf/ in kernels
73
- - kaggle_llm_template.py checks this path first before downloading from HF
74
-
75
- ### Architecture
76
- - Ollama: qwen2.5-coder:7b-instruct (CPU, ~4.5GB RAM, ~1-2 tok/s — acceptable for monitoring)
77
- - OpenClaw gateway: port 8080, nginx proxies 7860->8080
78
- - OpenClaw Ollama config: baseUrl MUST be http://127.0.0.1:11434/v1 (with /v1!), api: openai-responses
79
- - Telegram: configured via OpenClaw web UI after Space starts
80
-
81
- ### CONFIRMED WORKING (DO NOT CHANGE)
82
- 1. kaggle_template.py — FLUX.1-schnell image generation
83
- - Placeholders: {{PROMPTS_PLACEHOLDER}}, {{HF_TOKEN_PLACEHOLDER}}
84
- - Output: frame_0.png to frame_9.png in /kaggle/working
85
- 2. kaggle_tts_stt_template.py — StyleTTS2 + Whisper TTS
86
- - Placeholders: {{SCRIPT_PLACEHOLDER}}, {{VIDEO_SPEED_PLACEHOLDER}}
87
- - Output: voice.wav, word_timings.json in /kaggle/working
88
-
89
- ### Critical Bugs Already Fixed (must stay fixed)
90
- - ALL kernel-metadata.json: "is_private": "false" (string, not bool)
91
- - ALL kernel slugs: include random.randint(1000,9999) suffix to avoid 409 conflicts
92
- - HF Token: check both HF_TOKEN and HF_HUB_TOKEN env vars
93
- - Ollama baseUrl: must include /v1 suffix or OpenClaw gets empty responses
94
- - Ollama api field: must be "openai-responses" not default
95
-
96
- ### kaggle_llm_template.py Placeholders (NOT {{ }} format — plain text)
97
- - TOPICS_PLACEHOLDER → replaced with JSON array of used topics
98
- - FACTS_PLACEHOLDER → replaced with JSON array of used facts
99
- - FRAMES_PLACEHOLDER → replaced with NUM_FRAMES integer
100
- - HFTOKEN_PLACEHOLDER → replaced with quoted HF token string
101
-
102
- ### YouTube OAuth
103
- - token.pickle generated locally with auth_youtube.py
104
- - Uploaded to arshitmalik/yt-pipeline-data HF Dataset
105
- - Pipeline pulls it at startup via sync_hub.py
106
-
107
- ### Instructions for You (Next AI)
108
- 1. ALWAYS give terminal commands to write/edit files AND git push at the end of every response
109
- 2. NEVER change kaggle_template.py or kaggle_tts_stt_template.py (CONFIRMED WORKING)
110
- 3. NEVER use inline comments in commands (zsh doesn't like them)
111
- 4. git push format: https://arshitmalik:hf_REDACTED_SET_IN_HF_SECRETS@huggingface.co/spaces/arshitmalik/yt-openclaw
112
- 5. UPDATE THIS TRANSFER FILE in every response
113
- 6. When debugging: check /app/pipeline.log, /app/runs/*/error.log, /app/runs/*/state.txt
114
- 7. Kaggle kernels: ALWAYS "is_private": "false" + random int suffix in slug
115
- 8. If user wants new HF Space: create fresh, do NOT reuse yt-shorts-bot or yt-shorts-bot-v2
116
- 9. Space URL format: https://arshitmalik-yt-openclaw.hf.space
117
-
118
- ## ALL CURRENT FILE CONTENTS
119
-
120
- ### Dockerfile
121
- ```
122
- FROM ghcr.io/openclaw/openclaw:latest
123
-
124
- RUN apt-get update && apt-get install -y \
125
- python3 python3-pip ffmpeg curl git nginx \
126
- && rm -rf /var/lib/apt/lists/*
127
-
128
- RUN curl -fsSL https://ollama.com/install.sh | sh
129
-
130
- RUN pip3 install --break-system-packages \
131
- requests kaggle huggingface_hub \
132
- google-api-python-client google-auth \
133
- google-auth-oauthlib google-auth-httplib2 \
134
- pillow numpy
135
-
136
- RUN printf 'server {\n listen 7860;\n location / {\n proxy_pass http://127.0.0.1:8080;\n proxy_http_version 1.1;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection "upgrade";\n proxy_set_header Host $host;\n }\n}\n' > /etc/nginx/sites-available/default
137
-
138
- WORKDIR /app
139
- COPY . .
140
- RUN chmod +x start.sh
141
-
142
- EXPOSE 7860
143
- CMD ["/app/start.sh"]
144
-
145
- ```
146
-
147
- ### start.sh
148
- ```sh
149
- #!/bin/bash
150
- set -e
151
-
152
- echo "[boot] Configuring Kaggle credentials..."
153
- if [ -n "$KAGGLE_USERNAME" ] && [ -n "$KAGGLE_KEY" ]; then
154
- mkdir -p ~/.kaggle
155
- printf '{"username":"%s","key":"%s"}' "$KAGGLE_USERNAME" "$KAGGLE_KEY" > ~/.kaggle/kaggle.json
156
- chmod 600 ~/.kaggle/kaggle.json
157
- fi
158
-
159
- echo "[boot] Configuring YouTube client secrets..."
160
- if [ -n "$YOUTUBE_CLIENT_ID" ] && [ -n "$YOUTUBE_CLIENT_SECRET" ]; then
161
- python3 - << PYEOF
162
- import json, os
163
- d = {"installed": {
164
- "client_id": os.environ["YOUTUBE_CLIENT_ID"],
165
- "project_id": "yt-ai-bot",
166
- "auth_uri": "https://accounts.google.com/o/oauth2/auth",
167
- "token_uri": "https://oauth2.googleapis.com/token",
168
- "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
169
- "client_secret": os.environ["YOUTUBE_CLIENT_SECRET"],
170
- "redirect_uris": ["http://localhost"]
171
- }}
172
- open("/app/client_secrets.json", "w").write(json.dumps(d))
173
- print("[boot] client_secrets.json written")
174
- PYEOF
175
- fi
176
-
177
- echo "[boot] Pulling state from HF Dataset..."
178
- python3 - << PYEOF
179
- import sys; sys.path.insert(0, "/app")
180
- try:
181
- from sync_hub import pull_state
182
- from pathlib import Path
183
- pull_state(Path("/app"))
184
- except Exception as e:
185
- print(f"[boot] Hub pull skipped: {e}")
186
- PYEOF
187
-
188
- echo "[boot] Pre-configuring OpenClaw with Ollama..."
189
- mkdir -p ~/.openclaw
190
- GATEWAY_TOKEN="${OPENCLAW_PASSWORD:-arshit2025}"
191
- cat > ~/.openclaw/openclaw.json << JSONEOF
192
- {
193
- "models": {
194
- "providers": {
195
- "ollama": {
196
- "baseUrl": "http://127.0.0.1:11434/v1",
197
- "apiKey": "ollama-local",
198
- "api": "openai-responses",
199
- "models": [
200
- {
201
- "id": "qwen2.5-coder:7b-instruct",
202
- "name": "Qwen2.5-Coder 7B (local CPU)",
203
- "contextWindow": 8192,
204
- "maxTokens": 2048,
205
- "cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0},
206
- "input": ["text"]
207
- }
208
- ]
209
- }
210
- }
211
- },
212
- "agents": {
213
- "defaults": {
214
- "model": {"primary": "ollama/qwen2.5-coder:7b-instruct"}
215
- }
216
- },
217
- "gateway": {
218
- "port": 8080,
219
- "token": "$GATEWAY_TOKEN"
220
- }
221
- }
222
- JSONEOF
223
- cp /app/SOUL.md ~/.openclaw/SOUL.md
224
-
225
- echo "[boot] Starting nginx proxy (7860 -> 8080)..."
226
- nginx
227
-
228
- echo "[boot] Starting Ollama service..."
229
- OLLAMA_HOST=127.0.0.1 ollama serve &
230
- OLLAMA_PID=$!
231
-
232
- echo "[boot] Waiting for Ollama to be ready..."
233
- for i in $(seq 1 60); do
234
- if curl -sf http://127.0.0.1:11434/api/tags > /dev/null 2>&1; then
235
- echo "[boot] Ollama ready after ${i}s"
236
- break
237
- fi
238
- sleep 2
239
- done
240
-
241
- echo "[boot] Pulling qwen2.5-coder:7b-instruct (~4.5 GB, first boot only)..."
242
- ollama pull qwen2.5-coder:7b-instruct
243
-
244
- echo "[boot] Starting automation pipeline..."
245
- python3 /app/automation.py >> /app/pipeline.log 2>&1 &
246
- PIPE_PID=$!
247
- echo "[boot] Pipeline PID: $PIPE_PID"
248
-
249
- echo "[boot] Starting OpenClaw gateway on port 8080..."
250
- export OPENCLAW_API_PORT=8080
251
- export OLLAMA_HOST=http://127.0.0.1:11434
252
- openclaw gateway start &
253
- GW_PID=$!
254
-
255
- echo "[boot] All services started. Tailing pipeline log..."
256
- sleep 5
257
- tail -f /app/pipeline.log
258
-
259
- ```
260
-
261
- ### SOUL.md
262
- ```md
263
- # YouTube Shorts Science Bot — Pipeline Monitor
264
-
265
- You are the monitor and operator for Arshit's YouTube Shorts science channel automation pipeline.
266
-
267
- ## Your Setup
268
- - You run locally on a HuggingFace Docker Space (2 vCPU, 16 GB RAM)
269
- - The pipeline runs as a background process at /app/automation.py
270
- - Pipeline log: /app/pipeline.log
271
- - Runs directory: /app/runs/
272
- - State file per run: /app/runs/<run_name>/state.txt
273
- - Error logs: /app/runs/<run_name>/error.log
274
-
275
- ## Pipeline Stages (in order)
276
- 1. CONTENT — Kaggle kernel: DeepSeek-R1-32B generates topic, fact, script, prompts, tags
277
- 2. FRAMES — Kaggle kernel: FLUX.1-schnell generates 10 images
278
- 3. TTS — Kaggle kernel: StyleTTS2+Whisper generates voice.wav + word_timings.json
279
- 4. AUDIO — ffmpeg: speed adjustment (1.1x)
280
- 5. VIDEO — ffmpeg: assembles final 1080x1920 mp4 with ASS subtitles
281
- 6. UPLOAD — YouTube Data API v3 publishes the Short
282
- 7. DONE / FAILED
283
-
284
- ## Telegram Commands You Handle
285
-
286
- When the user says "status":
287
- Run: tail -n 40 /app/pipeline.log
288
- Then run: find /app/runs -name state.txt -exec sh -c 'echo "$(dirname $1 | xargs basename): $(cat $1)"' _ {} ; 2>/dev/null | head -10
289
- Summarise: current stage, last action, any errors.
290
-
291
- When the user says "logs" or "show logs":
292
- Run: tail -n 80 /app/pipeline.log
293
- Return the raw output.
294
-
295
- When the user says "errors":
296
- Run: find /app/runs -name error.log | xargs cat 2>/dev/null | tail -60
297
- Summarise what failed and in which stage.
298
-
299
- When the user says "run now":
300
- Run: touch /app/trigger_run
301
- Confirm: "Triggered. Next pipeline cycle will start within 5 minutes."
302
-
303
- When the user says "skip":
304
- Run: touch /app/trigger_skip
305
- Confirm: "Skipped current wait. Next run in 24 hours."
306
-
307
- When the user says "last video":
308
- Run: ls -t /app/runs/*/youtube_id.txt 2>/dev/null | head -1 | xargs cat 2>/dev/null
309
- Return: https://youtube.com/watch?v=<id>
310
-
311
- When the user says "disk" or "storage":
312
- Run: df -h /app && du -sh /app/runs/* 2>/dev/null | tail -20
313
- Return the output.
314
-
315
- When the user gives a code change instruction (e.g. "change video speed to 1.2x", "increase frames to 12"):
316
- 1. Read the relevant section of /app/automation.py
317
- 2. Make the precise edit
318
- 3. Kill the old pipeline: pkill -f "automation.py" || true
319
- 4. Restart it: python3 /app/automation.py >> /app/pipeline.log 2>&1 &
320
- 5. Confirm the change and the new PID.
321
-
322
- ## Rules
323
- - Always use shell commands to read actual files — never guess or fabricate output
324
- - Keep responses SHORT for mobile Telegram (3-5 lines max for status, longer only if asked)
325
- - When editing files, read before writing to avoid mistakes
326
- - After any file edit, always restart the pipeline process
327
- - If you cannot fix something, describe exactly what failed and what file to check
328
-
329
- ```
330
-
331
- ### README.md
332
- ```md
333
- ---
334
- title: Yt Openclaw
335
- emoji: 🦞
336
- colorFrom: red
337
- colorTo: orange
338
- sdk: docker
339
- pinned: false
340
- license: mit
341
- ---
342
-
343
- ```
344
-
345
- ### sync_hub.py
346
- ```py
347
- import os
348
- from pathlib import Path
349
-
350
- HF_TOKEN = os.environ.get("HF_TOKEN", "hf_REDACTED_SET_IN_HF_SECRETS")
351
- DATASET_REPO = os.environ.get("OPENCLAW_DATASET_REPO", "")
352
-
353
- PERSISTENT_FILES = {"topic_history.json", "fact_history.json", "token.pickle"}
354
-
355
- _api = None
356
- def _get_api():
357
- global _api
358
- if _api is None:
359
- from huggingface_hub import HfApi
360
- _api = HfApi(token=HF_TOKEN)
361
- return _api
362
-
363
- def pull_state(base_dir: Path):
364
- if not DATASET_REPO or not HF_TOKEN:
365
- print("OPENCLAW_DATASET_REPO not set, skipping Hub pull"); return
366
- from huggingface_hub import hf_hub_download
367
- from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
368
- print(f"Pulling state from {DATASET_REPO}...")
369
- for fname in PERSISTENT_FILES:
370
- try:
371
- hf_hub_download(repo_id=DATASET_REPO, filename=fname, repo_type="dataset",
372
- token=HF_TOKEN, local_dir=str(base_dir), local_dir_use_symlinks=False)
373
- print(f" pulled {fname}")
374
- except (EntryNotFoundError, RepositoryNotFoundError): pass
375
- except Exception as e: print(f" could not pull {fname}: {e}")
376
-
377
- def push_file(local_path: Path):
378
- if not DATASET_REPO or not HF_TOKEN: return
379
- if local_path.name not in PERSISTENT_FILES or not local_path.exists(): return
380
- try:
381
- _get_api().upload_file(
382
- path_or_fileobj=str(local_path), path_in_repo=local_path.name,
383
- repo_id=DATASET_REPO, repo_type="dataset", token=HF_TOKEN,
384
- commit_message=f"auto: {local_path.name}")
385
- print(f" synced {local_path.name} to Hub")
386
- except Exception as e:
387
- print(f" Hub sync failed for {local_path.name}: {e}")
388
-
389
- def push_all_state(base_dir: Path):
390
- for fname in PERSISTENT_FILES:
391
- p = base_dir / fname
392
- if p.exists(): push_file(p)
393
-
394
- ```
395
-
396
- ### automation.py
397
- ```py
398
- #!/usr/bin/env python3
399
- import os, json, time, subprocess, pickle, sys, shutil, re, random, traceback, logging
400
- from pathlib import Path
401
- from datetime import datetime, timezone
402
-
403
- BASE_DIR = Path(__file__).resolve().parent
404
- RUNS_DIR = BASE_DIR / "runs"
405
- KAGGLE_TEMPLATE = BASE_DIR / "kaggle_template.py"
406
- KAGGLE_TTS_TEMPLATE = BASE_DIR / "kaggle_tts_stt_template.py"
407
- KAGGLE_LLM_TEMPLATE = BASE_DIR / "kaggle_llm_template.py"
408
- TOPIC_HISTORY_FILE = BASE_DIR / "topic_history.json"
409
- FACT_HISTORY_FILE = BASE_DIR / "fact_history.json"
410
- KAGGLE_KEYS_FILE = BASE_DIR / "kaggle_keys.json"
411
- CLIENT_SECRETS_FILE = BASE_DIR / "client_secrets.json"
412
- TOKEN_FILE = BASE_DIR / "token.pickle"
413
- TRIGGER_RUN_FILE = BASE_DIR / "trigger_run"
414
- TRIGGER_SKIP_FILE = BASE_DIR / "trigger_skip"
415
-
416
- NUM_FRAMES = 10
417
- VIDEO_SPEED = 1.1
418
- SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
419
- RUNS_DIR.mkdir(parents=True, exist_ok=True)
420
-
421
- HF_TOKEN = os.environ.get("HF_TOKEN", os.environ.get("HF_HUB_TOKEN", "hf_REDACTED_SET_IN_HF_SECRETS"))
422
-
423
- logging.basicConfig(
424
- level=logging.INFO,
425
- format="[%(asctime)s] %(message)s",
426
- datefmt="%H:%M:%S",
427
- handlers=[
428
- logging.StreamHandler(sys.stdout),
429
- logging.FileHandler(BASE_DIR / "pipeline.log", encoding="utf-8"),
430
- ]
431
- )
432
- log = logging.getLogger()
433
-
434
- def load_kaggle_accounts():
435
- if KAGGLE_KEYS_FILE.exists():
436
- try:
437
- return json.loads(KAGGLE_KEYS_FILE.read_text()).get("accounts", [])
438
- except: pass
439
- u = os.environ.get("KAGGLE_USERNAME", "")
440
- k = os.environ.get("KAGGLE_KEY", "")
441
- return [{"username": u, "key": k}] if u and k else []
442
-
443
- KAGGLE_ACCOUNTS = load_kaggle_accounts()
444
-
445
- def set_kaggle_creds(account):
446
- kd = Path.home() / ".kaggle"
447
- kd.mkdir(exist_ok=True)
448
- (kd / "kaggle.json").write_text(json.dumps({"username": account["username"], "key": account["key"]}))
449
- (kd / "kaggle.json").chmod(0o600)
450
-
451
- def load_json_list(path):
452
- if path.exists():
453
- try:
454
- d = json.loads(path.read_text())
455
- return d if isinstance(d, list) else []
456
- except: pass
457
- return []
458
-
459
- def get_used_topics():
460
- return load_json_list(TOPIC_HISTORY_FILE)
461
-
462
- def get_used_facts():
463
- return load_json_list(FACT_HISTORY_FILE)
464
-
465
- def save_topic(topic):
466
- h = get_used_topics()
467
- h.append({"topic": topic, "date": datetime.now().isoformat()})
468
- TOPIC_HISTORY_FILE.write_text(json.dumps(h[-200:], ensure_ascii=False, indent=2))
469
- try:
470
- from sync_hub import push_file
471
- push_file(TOPIC_HISTORY_FILE)
472
- except: pass
473
-
474
- def save_fact(fact, topic=""):
475
- h = get_used_facts()
476
- h.append({"fact": fact, "topic": topic, "date": datetime.now().isoformat()})
477
- FACT_HISTORY_FILE.write_text(json.dumps(h[-200:], ensure_ascii=False, indent=2))
478
- try:
479
- from sync_hub import push_file
480
- push_file(FACT_HISTORY_FILE)
481
- except: pass
482
-
483
- def load_state(run: Path) -> str:
484
- f = run / "state.txt"
485
- return f.read_text().strip() if f.exists() else "CONTENT"
486
-
487
- def save_state(run: Path, state: str):
488
- (run / "state.txt").write_text(state)
489
- log.info(f"State -> {state}")
490
-
491
- def save_error_log(run: Path, exc: Exception, stage: str):
492
- (run / "error.log").write_text(
493
- f"[{datetime.now().isoformat()}] Stage: {stage}\n{traceback.format_exc()}")
494
-
495
- def atomic_write(path: Path, data):
496
- tmp = path.with_suffix(".tmp")
497
- txt = json.dumps(data, ensure_ascii=False, indent=2) if isinstance(data, (dict, list)) else str(data)
498
- tmp.write_text(txt)
499
- tmp.replace(path)
500
-
501
- def push_kaggle_kernel(build_dir: Path, main_py_text: str, label: str):
502
- (build_dir / "main.py").write_text(main_py_text)
503
- suffix = build_dir.parent.name.replace("_", "")[-10:]
504
- accounts = KAGGLE_ACCOUNTS.copy()
505
- random.shuffle(accounts)
506
- for acc in accounts:
507
- tag = label[:5].replace(" ", "")
508
- slug = f"{acc['username']}/yt-{tag}-{suffix}-{random.randint(1000, 9999)}"
509
- meta = {
510
- "id": slug,
511
- "title": f"YT {label} {suffix}",
512
- "code_file": "main.py",
513
- "language": "python",
514
- "kernel_type": "script",
515
- "enable_gpu": "true",
516
- "enable_internet": "true",
517
- "is_private": "false"
518
- }
519
- (build_dir / "kernel-metadata.json").write_text(json.dumps(meta, indent=2))
520
- set_kaggle_creds(acc)
521
- r = subprocess.run(["kaggle", "kernels", "push", "-p", str(build_dir)],
522
- capture_output=True, text=True)
523
- if r.returncode == 0:
524
- (build_dir / "kaggle_account.json").write_text(
525
- json.dumps({"username": acc["username"], "slug": slug}))
526
- log.info(f"Pushed {label} ({acc['username']}): {slug}")
527
- return
528
- log.error(f"Push failed ({acc['username']}): {r.stderr.strip()[:200]}")
529
- raise RuntimeError(f"{label} kernel push failed on all accounts")
530
-
531
- def wait_kaggle(slug: str, timeout: int = 3600):
532
- start = time.time()
533
- while time.time() - start < timeout:
534
- elapsed = int(time.time() - start)
535
- r = subprocess.run(["kaggle", "kernels", "status", slug],
536
- capture_output=True, text=True)
537
- status = r.stdout.strip().lower()
538
- lines = [l for l in status.split("\n") if "has status" in l]
539
- cur = lines[0] if lines else status[:120]
540
- log.info(f" {elapsed//60:02d}:{elapsed%60:02d} | {cur}")
541
- if "complete" in status:
542
- log.info(f"Kernel done ({elapsed//60}m {elapsed%60}s)")
543
- return
544
- if "error" in status or "failed" in status:
545
- raise RuntimeError(f"Kernel failed: {status}")
546
- time.sleep(30)
547
- raise RuntimeError(f"Kernel timed out after {timeout//60}min")
548
-
549
- def step_content(run: Path):
550
- log.info("=" * 56)
551
- log.info("STEP 1: CONTENT (Kaggle DeepSeek-R1-32B from Dataset)")
552
- log.info("=" * 56)
553
- build_dir = run / "kaggle_llm_build"
554
- if not build_dir.exists():
555
- build_dir.mkdir()
556
- used_topics = [e.get("topic", "") for e in get_used_topics()[-60:] if e.get("topic")]
557
- used_facts = get_used_facts()[-20:]
558
- tmpl = KAGGLE_LLM_TEMPLATE.read_text()
559
- tmpl = tmpl.replace("TOPICS_PLACEHOLDER", json.dumps(used_topics, indent=2))
560
- tmpl = tmpl.replace("FACTS_PLACEHOLDER", json.dumps(used_facts, indent=2))
561
- tmpl = tmpl.replace("FRAMES_PLACEHOLDER", str(NUM_FRAMES))
562
- tmpl = tmpl.replace("HFTOKEN_PLACEHOLDER", json.dumps(HF_TOKEN))
563
- push_kaggle_kernel(build_dir, tmpl, "llm")
564
-
565
- info = json.loads((build_dir / "kaggle_account.json").read_text())
566
- for acc in KAGGLE_ACCOUNTS:
567
- if acc["username"] == info["username"]:
568
- set_kaggle_creds(acc); break
569
-
570
- log.info(f"Waiting for LLM kernel {info['slug']} ...")
571
- log.info(" (Dataset mount ~0min + inference ~3-5min)")
572
- wait_kaggle(info["slug"])
573
-
574
- log.info("Downloading content.json ...")
575
- subprocess.run(["kaggle", "kernels", "output", info["slug"], "-p", str(run)],
576
- check=True, capture_output=True)
577
-
578
- candidates = list(run.glob("**/content.json"))
579
- if not candidates:
580
- raise RuntimeError("content.json not found in kernel output")
581
- src = candidates[0]
582
- dst = run / "content.json"
583
- if src != dst: shutil.move(str(src), str(dst))
584
-
585
- data = json.loads(dst.read_text())
586
- missing = [k for k in ("topic", "fact", "hook", "title", "script", "prompts", "tags")
587
- if not data.get(k)]
588
- if missing:
589
- raise RuntimeError(f"content.json missing: {missing}")
590
- if len(data["prompts"]) < NUM_FRAMES:
591
- raise RuntimeError(f"Only {len(data['prompts'])}/{NUM_FRAMES} prompts")
592
-
593
- save_topic(data["topic"])
594
- save_fact(data["fact"], topic=data["topic"])
595
- log.info(f"Content OK: topic={data['topic']} | {data['script_char_count']}c | {len(data['prompts'])} prompts")
596
- shutil.rmtree(build_dir, ignore_errors=True)
597
- save_state(run, "FRAMES")
598
-
599
- def step_frames(run: Path):
600
- log.info("=" * 56)
601
- log.info("STEP 2: IMAGES (Kaggle FLUX.1-schnell)")
602
- log.info("=" * 56)
603
- build_dir = run / "kaggle_frames_build"
604
- if not build_dir.exists():
605
- build_dir.mkdir()
606
- data = json.loads((run / "content.json").read_text())
607
- tmpl = KAGGLE_TEMPLATE.read_text()
608
- tmpl = tmpl.replace("{{PROMPTS_PLACEHOLDER}}", json.dumps(data["prompts"], indent=2))
609
- tmpl = tmpl.replace("{{HF_TOKEN_PLACEHOLDER}}", json.dumps(HF_TOKEN))
610
- push_kaggle_kernel(build_dir, tmpl, "frames")
611
-
612
- info = json.loads((build_dir / "kaggle_account.json").read_text())
613
- for acc in KAGGLE_ACCOUNTS:
614
- if acc["username"] == info["username"]:
615
- set_kaggle_creds(acc); break
616
- wait_kaggle(info["slug"])
617
- subprocess.run(["kaggle", "kernels", "output", info["slug"], "-p", str(run)],
618
- check=True, capture_output=True)
619
- frames = sorted(run.glob("**/frame_*.png"))
620
- if len(frames) < NUM_FRAMES:
621
- raise RuntimeError(f"Only {len(frames)}/{NUM_FRAMES} frames")
622
- for f in frames:
623
- if f.parent != run: shutil.move(str(f), str(run / f.name))
624
- log.info(f"Downloaded {len(frames)} frames")
625
- shutil.rmtree(build_dir, ignore_errors=True)
626
- save_state(run, "TTS")
627
-
628
- def step_tts(run: Path):
629
- log.info("=" * 56)
630
- log.info("STEP 3: TTS (Kaggle StyleTTS2 + Whisper)")
631
- log.info("=" * 56)
632
- build_dir = run / "kaggle_tts_build"
633
- if not build_dir.exists():
634
- build_dir.mkdir()
635
- data = json.loads((run / "content.json").read_text())
636
- tmpl = KAGGLE_TTS_TEMPLATE.read_text()
637
- tmpl = tmpl.replace("{{SCRIPT_PLACEHOLDER}}", json.dumps(data["script"]))
638
- tmpl = tmpl.replace("{{VIDEO_SPEED_PLACEHOLDER}}", str(VIDEO_SPEED))
639
- push_kaggle_kernel(build_dir, tmpl, "tts")
640
-
641
- info = json.loads((build_dir / "kaggle_account.json").read_text())
642
- for acc in KAGGLE_ACCOUNTS:
643
- if acc["username"] == info["username"]:
644
- set_kaggle_creds(acc); break
645
- wait_kaggle(info["slug"])
646
- subprocess.run(["kaggle", "kernels", "output", info["slug"], "-p", str(run)],
647
- check=True, capture_output=True)
648
- voice = next(iter(run.glob("**/voice.wav")), None)
649
- timings = next(iter(run.glob("**/word_timings.json")), None)
650
- if not voice: raise RuntimeError("voice.wav not found in TTS output")
651
- if not timings: raise RuntimeError("word_timings.json not found in TTS output")
652
- if voice.parent != run: shutil.move(str(voice), str(run / "voice.wav"))
653
- if timings.parent != run: shutil.move(str(timings), str(run / "word_timings.json"))
654
- log.info("TTS done: voice.wav + word_timings.json")
655
- shutil.rmtree(build_dir, ignore_errors=True)
656
- save_state(run, "AUDIO")
657
-
658
- def step_audio(run: Path):
659
- log.info("STEP 4: AUDIO PROCESSING")
660
- voice_fast = run / "voice_fast.wav"
661
- r = subprocess.run(
662
- ["ffmpeg", "-y", "-i", str(run / "voice.wav"),
663
- "-filter:a", f"atempo={VIDEO_SPEED}", str(voice_fast)],
664
- capture_output=True, text=True)
665
- if r.returncode != 0:
666
- raise RuntimeError(f"ffmpeg speed failed: {r.stderr[:300]}")
667
- r2 = subprocess.run(
668
- ["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
669
- "-of", "csv=p=0", str(voice_fast)],
670
- capture_output=True, text=True, check=True)
671
- audio_dur = float(r2.stdout.strip())
672
- timings = json.loads((run / "word_timings.json").read_text())
673
- adj = [{"word": w["word"], "start": w["start"] / VIDEO_SPEED, "end": w["end"] / VIDEO_SPEED}
674
- for w in timings]
675
- atomic_write(run / "adjusted_timings.json", adj)
676
- atomic_write(run / "audio_duration.txt", str(audio_dur))
677
- log.info(f"Audio: {audio_dur:.2f}s at {VIDEO_SPEED}x")
678
- save_state(run, "VIDEO")
679
-
680
- def build_ass(timings, out_path: Path, W=1080, H=1920):
681
- def ts(s):
682
- h = int(s // 3600); m = int((s % 3600) // 60); sec = s % 60
683
- return f"{h}:{m:02d}:{sec:05.2f}"
684
- header = (
685
- "[Script Info]\nScriptType: v4.00+\n"
686
- f"PlayResX: {W}\nPlayResY: {H}\nScaledBorderAndShadow: yes\n\n"
687
- "[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, "
688
- "OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, "
689
- "Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
690
- "Style: Default,Arial,72,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,"
691
- "-1,0,0,0,100,100,0,0,1,3,2,2,50,50,80,1\n\n"
692
- "[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n")
693
- lines = [header]
694
- for chunk in [timings[i:i+5] for i in range(0, len(timings), 5)]:
695
- if not chunk: continue
696
- text = " ".join(w["word"] for w in chunk)
697
- lines.append(f"Dialogue: 0,{ts(chunk[0]['start'])},{ts(chunk[-1]['end'])},Default,,0,0,0,,{text}\n")
698
- out_path.write_text("".join(lines), encoding="utf-8")
699
-
700
- def step_video(run: Path):
701
- log.info("STEP 5: VIDEO ASSEMBLY")
702
- adj = json.loads((run / "adjusted_timings.json").read_text())
703
- dur = float((run / "audio_duration.txt").read_text().strip())
704
- frames = sorted(run.glob("frame_*.png"))
705
- if not frames: raise RuntimeError("No frames found")
706
- W, H = 1080, 1920
707
- frame_dur = dur / len(frames)
708
- concat_file = run / "frames.txt"
709
- with open(concat_file, "w") as f:
710
- for fr in frames:
711
- f.write(f"file '{fr}'\nduration {frame_dur:.6f}\n")
712
- f.write(f"file '{frames[-1]}'\n")
713
- slides = run / "slides.mp4"
714
- r = subprocess.run([
715
- "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", str(concat_file),
716
- "-vf", f"scale={W}:{H}:force_original_aspect_ratio=increase,crop={W}:{H}",
717
- "-c:v", "libx264", "-preset", "fast", "-pix_fmt", "yuv420p", "-r", "30", str(slides)
718
- ], capture_output=True, text=True)
719
- if r.returncode != 0: raise RuntimeError(f"Slides failed: {r.stderr[:400]}")
720
- ass = run / "subtitles.ass"
721
- build_ass(adj, ass, W, H)
722
- final = run / "final.mp4"
723
- r = subprocess.run([
724
- "ffmpeg", "-y", "-i", str(slides), "-i", str(run / "voice_fast.wav"),
725
- "-vf", f"ass={ass}",
726
- "-c:v", "libx264", "-preset", "fast", "-c:a", "aac", "-b:a", "192k",
727
- "-shortest", str(final)
728
- ], capture_output=True, text=True)
729
- if r.returncode != 0: raise RuntimeError(f"Final video failed: {r.stderr[:400]}")
730
- log.info(f"Final video: {final.stat().st_size / 1024 / 1024:.1f} MB")
731
- save_state(run, "UPLOAD")
732
-
733
- def step_upload(run: Path):
734
- log.info("STEP 6: YOUTUBE UPLOAD")
735
- data = json.loads((run / "content.json").read_text())
736
- final = run / "final.mp4"
737
- if not final.exists(): raise RuntimeError("final.mp4 not found")
738
- try:
739
- from sync_hub import pull_state
740
- pull_state(BASE_DIR)
741
- except: pass
742
- creds = None
743
- if TOKEN_FILE.exists():
744
- with open(TOKEN_FILE, "rb") as f:
745
- creds = pickle.load(f)
746
- if not creds or not creds.valid:
747
- if creds and creds.expired and creds.refresh_token:
748
- from google.auth.transport.requests import Request
749
- creds.refresh(Request())
750
- with open(TOKEN_FILE, "wb") as f: pickle.dump(creds, f)
751
- else:
752
- raise RuntimeError(
753
- "token.pickle missing or invalid. Run auth_youtube.py on your Mac, "
754
- "then upload token.pickle to HF Dataset arshitmalik/yt-pipeline-data")
755
- try:
756
- from sync_hub import push_file
757
- push_file(TOKEN_FILE)
758
- except: pass
759
- from googleapiclient.discovery import build
760
- from googleapiclient.http import MediaFileUpload
761
- yt = build("youtube", "v3", credentials=creds)
762
- body = {
763
- "snippet": {
764
- "title": data.get("title", "Science Fact")[:100],
765
- "description": data.get("fact", "") + "\n\n" + "\n".join(data.get("tags", [])),
766
- "tags": data.get("tags", []),
767
- "categoryId": "28",
768
- "defaultLanguage": "en"
769
- },
770
- "status": {"privacyStatus": "public", "selfDeclaredMadeForKids": False}
771
- }
772
- media = MediaFileUpload(str(final), chunksize=4 * 1024 * 1024, resumable=True)
773
- req = yt.videos().insert(part="snippet,status", body=body, media_body=media)
774
- log.info("Uploading to YouTube...")
775
- resp = None
776
- while resp is None:
777
- status, resp = req.next_chunk()
778
- if status: log.info(f"Upload progress: {int(status.progress() * 100)}%")
779
- vid_id = resp.get("id", "unknown")
780
- log.info(f"Published: https://youtube.com/watch?v={vid_id}")
781
- atomic_write(run / "youtube_id.txt", vid_id)
782
- try:
783
- from sync_hub import push_all_state
784
- push_all_state(BASE_DIR)
785
- except: pass
786
- save_state(run, "DONE")
787
-
788
- STEPS = {
789
- "CONTENT": step_content,
790
- "FRAMES": step_frames,
791
- "TTS": step_tts,
792
- "AUDIO": step_audio,
793
- "VIDEO": step_video,
794
- "UPLOAD": step_upload,
795
- }
796
-
797
- def main():
798
- log.info("=" * 56)
799
- log.info("YouTube Shorts Pipeline — STARTING")
800
- log.info(f"DeepSeek-R1-32B (Kaggle) | FLUX (Kaggle) | StyleTTS2 (Kaggle)")
801
- log.info(f"Frames: {NUM_FRAMES} | Speed: {VIDEO_SPEED}x | Accounts: {len(KAGGLE_ACCOUNTS)}")
802
- log.info("=" * 56)
803
- try:
804
- from sync_hub import pull_state
805
- pull_state(BASE_DIR)
806
- except: pass
807
-
808
- NEXT_RUN_AFTER = time.time() + 15
809
-
810
- while True:
811
- if TRIGGER_SKIP_FILE.exists():
812
- TRIGGER_SKIP_FILE.unlink(missing_ok=True)
813
- NEXT_RUN_AFTER = time.time() + 86400
814
- log.info("Skipped. Next run in 24h.")
815
- time.sleep(60); continue
816
-
817
- if TRIGGER_RUN_FILE.exists():
818
- TRIGGER_RUN_FILE.unlink(missing_ok=True)
819
- NEXT_RUN_AFTER = 0
820
- log.info("Triggered immediate run!")
821
-
822
- if time.time() < NEXT_RUN_AFTER:
823
- left = NEXT_RUN_AFTER - time.time()
824
- if int(left) % 300 < 30:
825
- log.info(f"Next run in {left/3600:.1f}h ...")
826
- time.sleep(30); continue
827
-
828
- run = None
829
- if RUNS_DIR.exists():
830
- for d in sorted(RUNS_DIR.iterdir(), key=lambda x: x.stat().st_mtime, reverse=True):
831
- if d.is_dir() and load_state(d) not in ("DONE", "FAILED"):
832
- run = d; break
833
- if not run:
834
- run = RUNS_DIR / datetime.now().strftime("%Y%m%d_%H%M%S")
835
- run.mkdir(parents=True)
836
- save_state(run, "CONTENT")
837
-
838
- state = load_state(run)
839
- log.info(f"\nRun: {run.name} Stage: {state}")
840
- try:
841
- if state in STEPS:
842
- STEPS[state](run)
843
- else:
844
- save_state(run, "FAILED")
845
- except KeyboardInterrupt:
846
- log.info("Interrupted"); sys.exit(0)
847
- except Exception as e:
848
- log.error(f"Error in {state}: {e}")
849
- traceback.print_exc()
850
- save_error_log(run, e, state)
851
- save_state(run, "FAILED")
852
-
853
- cur = load_state(run)
854
- if cur == "DONE":
855
- content = json.loads((run / "content.json").read_text())
856
- yt_id = (run / "youtube_id.txt").read_text().strip() if (run / "youtube_id.txt").exists() else "?"
857
- log.info(f"\nCOMPLETE: {content.get('title', '')} — https://youtube.com/watch?v={yt_id}")
858
- try: shutil.rmtree(run)
859
- except: pass
860
- NEXT_RUN_AFTER = time.time() + 86400
861
- log.info("Next run in 24h.")
862
- elif cur == "FAILED":
863
- log.error(f"FAILED — check: {run}")
864
- NEXT_RUN_AFTER = time.time() + 300
865
- log.info("Retrying in 5 min.")
866
-
867
- if __name__ == "__main__":
868
- main()
869
-
870
- ```
871
-
872
- ### kaggle_llm_template.py
873
- ```py
874
- #!/usr/bin/env python3
875
- import os, sys, json, re, subprocess, gc
876
- from pathlib import Path
877
-
878
- OUTPUT_DIR = Path("/kaggle/working")
879
- OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
880
-
881
- USED_TOPICS = TOPICS_PLACEHOLDER
882
- USED_FACTS = FACTS_PLACEHOLDER
883
- NUM_FRAMES = FRAMES_PLACEHOLDER
884
- HF_TOKEN = HFTOKEN_PLACEHOLDER
885
-
886
- NUM_FRAMES = int(NUM_FRAMES)
887
-
888
- print("=" * 60)
889
- print("DeepSeek-R1-32B Content Generator (Kaggle Dataset Cache)")
890
- print("=" * 60)
891
-
892
- DATASET_MODEL_PATH = Path("/kaggle/input/deepseek-r1-32b-gguf/DeepSeek-R1-Distill-Qwen-32B-IQ3_XS.gguf")
893
- MODEL_DOWNLOAD_DIR = Path("/kaggle/temp/model")
894
-
895
- if DATASET_MODEL_PATH.exists():
896
- model_path = str(DATASET_MODEL_PATH)
897
- print(f"Using cached model from Dataset: {model_path}")
898
- print("Compiling llama-cpp-python for sm_60...")
899
- os.environ["CMAKE_ARGS"] = "-DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=60"
900
- os.environ["FORCE_CMAKE"] = "1"
901
- subprocess.check_call(
902
- [sys.executable, "-m", "pip", "install", "llama-cpp-python", "--no-cache-dir", "-q"],
903
- stdout=subprocess.DEVNULL)
904
- else:
905
- print("Dataset not found, downloading from HF Hub...")
906
- os.environ["CMAKE_ARGS"] = "-DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=60"
907
- os.environ["FORCE_CMAKE"] = "1"
908
- subprocess.check_call(
909
- [sys.executable, "-m", "pip", "install", "llama-cpp-python", "huggingface_hub", "--no-cache-dir", "-q"],
910
- stdout=subprocess.DEVNULL)
911
- from huggingface_hub import hf_hub_download
912
- kw = dict(
913
- repo_id="bartowski/DeepSeek-R1-Distill-Qwen-32B-GGUF",
914
- filename="DeepSeek-R1-Distill-Qwen-32B-IQ3_XS.gguf",
915
- local_dir=str(MODEL_DOWNLOAD_DIR))
916
- if HF_TOKEN:
917
- kw["token"] = HF_TOKEN
918
- model_path = hf_hub_download(**kw)
919
- print(f"Downloaded to: {model_path}")
920
-
921
- print("\nLoading model...")
922
- from llama_cpp import Llama
923
- llm = Llama(
924
- model_path=model_path,
925
- n_gpu_layers=-1,
926
- n_ctx=4096,
927
- n_batch=256,
928
- n_ubatch=256,
929
- flash_attn=False,
930
- offload_kqv=False,
931
- type_k="q4_0",
932
- type_v="q4_0",
933
- verbose=False)
934
- print("Model loaded\n")
935
-
936
- _STOP = ["<|im_end|>", "<|end_of_text|>", "</s>"]
937
-
938
- def infer(prompt, max_tokens=600, temperature=0.35):
939
- resp = llm.create_chat_completion(
940
- messages=[{"role": "user", "content": prompt}],
941
- max_tokens=max_tokens, temperature=temperature, stop=_STOP)
942
- text = resp["choices"][0]["message"]["content"] or ""
943
- text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip()
944
- return text
945
-
946
- print("=" * 60 + "\nGenerating content...\n" + "=" * 60)
947
-
948
- used_str = "\n".join(f"- {t}" for t in USED_TOPICS[-60:]) or "none yet"
949
-
950
- TOPIC = ""
951
- for att in range(8):
952
- raw = infer(
953
- "Viral YouTube Shorts science content creator.\n\n"
954
- "Topics already used - DO NOT repeat any:\n" + used_str + "\n\n"
955
- "Generate exactly ONE new unique topic for a 60-second science Short.\n"
956
- "Requirements: 2-6 words, physics/space/cosmology/quantum only, "
957
- "makes viewers say WHAT or NO WAY, no living creatures or people, "
958
- "never: quantum tunneling, parallel universe, dark energy, big bang.\n"
959
- "Output ONLY the topic (2-6 words):",
960
- max_tokens=50, temperature=0.35 + att * 0.05)
961
- topic = re.sub(r"[^a-zA-Z0-9 \-]", "", raw.lower().strip().strip('"\'*- ')).strip()
962
- if 2 <= len(topic.split()) <= 6 and 5 <= len(topic) <= 60:
963
- TOPIC = topic
964
- print(f"Topic: {TOPIC}")
965
- break
966
- print(f" bad topic att {att+1}: '{raw[:50]}'")
967
- if not TOPIC:
968
- raise RuntimeError("Could not generate valid topic in 8 attempts")
969
-
970
- recent_facts = "; ".join(e.get("fact", "")[:55] for e in USED_FACTS[-5:]) or "none"
971
-
972
- FACT = ""
973
- for att in range(6):
974
- raw = infer(
975
- "Scientific fact writer for viral science videos.\n\n"
976
- f"Topic: {TOPIC}\n"
977
- f"Recent facts used (use DIFFERENT angle): {recent_facts}\n\n"
978
- "Write ONE astonishing scientific fact about this topic.\n"
979
- "Rules: 1-2 sentences, include extreme number/comparison, 100% accurate, "
980
- "no people/animals, start directly with fact.\nOutput ONLY the fact:",
981
- max_tokens=200, temperature=0.30 + att * 0.05)
982
- fact = raw.strip().strip('"\'')
983
- bad = ["as an ai", "i will", "let me", "here is", "sure!", "certainly", "of course"]
984
- if any(b in fact.lower() for b in bad):
985
- print(f" meta text att {att+1}"); continue
986
- if len(fact.split()) < 8:
987
- print(f" too short att {att+1}"); continue
988
- FACT = fact
989
- print(f"Fact: {FACT[:100]}...")
990
- break
991
- if not FACT:
992
- raise RuntimeError("Could not generate valid fact in 6 attempts")
993
-
994
- HOOK = infer(
995
- f"Fact: {FACT}\n\nWrite ONE hook that stops mid-scroll.\n"
996
- "Must start with: What if..., Imagine..., or Did you know...\nOutput ONLY the hook:",
997
- max_tokens=120, temperature=0.35).strip().strip('"\'')
998
- print(f"Hook: {HOOK[:90]}...")
999
-
1000
- raw_script = infer(
1001
- f"Topic: {TOPIC}\nHook: {HOOK}\nFact: {FACT}\n\n"
1002
- "Write a 60-second voiceover script. Plain text only, no brackets or stage directions, "
1003
- "900-1050 characters, flow: hook->fact->details->CTA. "
1004
- "Final sentence MUST be: Follow for more mind-blowing science facts!\nOutput ONLY the script:",
1005
- max_tokens=1100, temperature=0.35)
1006
- SCRIPT = re.sub(r"\s+", " ", re.sub(r"\[.*?\]", "", raw_script)).strip()
1007
- if len(SCRIPT) > 1200: SCRIPT = SCRIPT[:1200].rsplit(" ", 1)[0]
1008
- if "follow for more" not in SCRIPT.lower():
1009
- end = " Follow for more mind-blowing science facts!"
1010
- SCRIPT = SCRIPT + end if len(SCRIPT) + len(end) <= 1200 else SCRIPT[:1200 - len(end)].rsplit(" ", 1)[0] + end
1011
- print(f"Script: {len(SCRIPT.split())}w {len(SCRIPT)}c")
1012
-
1013
- TITLE = infer(
1014
- f"Topic: {TOPIC}\nScript preview: {SCRIPT[:200]}\n\n"
1015
- "Write ONE viral YouTube Shorts title. KEY WORDS in ALL CAPS, 1-2 emojis, "
1016
- "include number if possible, max 100 chars, scientifically accurate.\nOutput ONLY the title:",
1017
- max_tokens=100, temperature=0.35).strip().strip('"\'')[:100]
1018
- print(f"Title: {TITLE}")
1019
-
1020
- PROMPTS = []
1021
- for att in range(5):
1022
- raw = infer(
1023
- f"Topic: {TOPIC}\nScript: {SCRIPT}\n\n"
1024
- f"Create EXACTLY {NUM_FRAMES} cosmic/space image prompts.\n"
1025
- "Rules: 15-25 words each, ZERO humans/faces/text, different visual each, "
1026
- "end every prompt with: cinematic lighting, highly detailed, 8k\n"
1027
- f"Output ONLY the numbered list 1 to {NUM_FRAMES}:",
1028
- max_tokens=1600, temperature=0.35)
1029
- parsed = [re.sub(r"^\d+[\.\)]\s*", "", l.strip())
1030
- for l in raw.split("\n") if re.match(r"^\d+[\.\)]\s+\S", l.strip())]
1031
- parsed = [p for p in parsed if len(p.split()) >= 5]
1032
- if len(parsed) >= NUM_FRAMES:
1033
- PROMPTS = parsed[:NUM_FRAMES]; break
1034
- print(f" got {len(parsed)}/{NUM_FRAMES} prompts att {att+1}")
1035
- if len(PROMPTS) < NUM_FRAMES:
1036
- raise RuntimeError(f"Only {len(PROMPTS)}/{NUM_FRAMES} image prompts")
1037
-
1038
- raw_tags = infer(
1039
- f"Generate 12 hashtags for a science Short about: {TOPIC}\n"
1040
- "Each starts with #, lowercase no spaces inside, relevant to physics/space/science.\n"
1041
- "Output ONLY hashtags one per line:",
1042
- max_tokens=280, temperature=0.3)
1043
- TAGS_gen = ["#" + t.strip().strip("#").lower().replace(" ", "")
1044
- for t in raw_tags.split("\n") if t.strip().startswith("#")][:12]
1045
- TAGS = list(dict.fromkeys(["#shorts", "#science", "#space", "#physics", "#physicsfacts"] + TAGS_gen))[:15]
1046
- print(f"{len(TAGS)} tags")
1047
-
1048
- import datetime as _dt
1049
- content = {
1050
- "topic": TOPIC, "fact": FACT, "hook": HOOK, "title": TITLE, "script": SCRIPT,
1051
- "script_word_count": len(SCRIPT.split()), "script_char_count": len(SCRIPT),
1052
- "tags": TAGS, "prompts": PROMPTS,
1053
- "timestamp": _dt.datetime.now().isoformat()
1054
- }
1055
- out = OUTPUT_DIR / "content.json"
1056
- out.write_text(json.dumps(content, indent=2, ensure_ascii=False))
1057
- print(f"\n{'='*60}")
1058
- print(f"content.json saved ({out.stat().st_size} bytes)")
1059
- print(f" Topic: {TOPIC}")
1060
- print(f" Title: {TITLE}")
1061
- print(f" Script: {len(SCRIPT)} chars / {len(SCRIPT.split())} words")
1062
- print(f" Frames: {len(PROMPTS)}")
1063
- print("=" * 60)
1064
-
1065
- ```
1066
-
1067
- ### auth_youtube.py
1068
- ```py
1069
- #!/usr/bin/env python3
1070
- """
1071
- Run this ONCE on your Mac to generate token.pickle.
1072
- Then upload it to HF Dataset: arshitmalik/yt-pipeline-data
1073
- """
1074
- import pickle, json
1075
- from pathlib import Path
1076
- from google_auth_oauthlib.flow import InstalledAppFlow
1077
-
1078
- SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
1079
-
1080
- import os
1081
- cid = os.environ.get("YOUTUBE_CLIENT_ID", "")
1082
- cs = os.environ.get("YOUTUBE_CLIENT_SECRET", "")
1083
- if cid and cs and not Path("client_secrets.json").exists():
1084
- Path("client_secrets.json").write_text(json.dumps({"installed": {
1085
- "client_id": cid, "project_id": "yt-ai-bot",
1086
- "auth_uri": "https://accounts.google.com/o/oauth2/auth",
1087
- "token_uri": "https://oauth2.googleapis.com/token",
1088
- "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
1089
- "client_secret": cs, "redirect_uris": ["http://localhost"]}}))
1090
-
1091
- flow = InstalledAppFlow.from_client_secrets_file("client_secrets.json", SCOPES)
1092
- creds = flow.run_local_server(port=0)
1093
- with open("token.pickle", "wb") as f:
1094
- pickle.dump(creds, f)
1095
- print("token.pickle saved! Now run the upload command below.")
1096
-
1097
- ```
1098
-
1099
- ### kaggle_keys.json
1100
- ```json
1101
- {"accounts":[{"username":"arshitmalik","key":"REDACTED_SET_IN_KAGGLE_KEYS_JSON"}]}
1102
-
1103
- ```
1104
-
1105
- ### kaggle_template.py
1106
- ```py
1107
- # kaggle_template.py
1108
- # =====================================================
1109
- # KAGGLE IMAGE WORKER (FLUX.1-SCHNELL)
1110
- # =====================================================
1111
- import os, torch, gc, subprocess, sys
1112
-
1113
- # Install bitsandbytes if missing (Critical for T4 GPU)
1114
- try:
1115
- import bitsandbytes
1116
- except ImportError:
1117
- subprocess.check_call([sys.executable, "-m", "pip", "install", "-U", "bitsandbytes"])
1118
-
1119
- from diffusers import FluxPipeline, FluxTransformer2DModel, BitsAndBytesConfig
1120
- from pathlib import Path
1121
-
1122
- # --- CONFIG ---
1123
- # The automation script will inject the prompts here automatically
1124
- PROMPTS = [
1125
- # {{PROMPTS_PLACEHOLDER}}
1126
- ]
1127
-
1128
- OUTPUT_DIR = Path("/kaggle/working/images")
1129
- OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
1130
-
1131
- # --- MODEL LOADING ---
1132
- print("📦 Loading Quantized Flux...")
1133
-
1134
- # 4-bit config to fit Flux on Kaggle T4s
1135
- bnb_config = BitsAndBytesConfig(
1136
- load_in_4bit=True,
1137
- bnb_4bit_quant_type="nf4",
1138
- bnb_4bit_compute_dtype=torch.bfloat16,
1139
- )
1140
-
1141
- # Load Transformer
1142
- transformer = FluxTransformer2DModel.from_pretrained(
1143
- "black-forest-labs/FLUX.1-schnell",
1144
- subfolder="transformer",
1145
- quantization_config=bnb_config,
1146
- torch_dtype=torch.bfloat16,
1147
- low_cpu_mem_usage=True
1148
- )
1149
-
1150
- # Load Pipeline
1151
- pipe = FluxPipeline.from_pretrained(
1152
- "black-forest-labs/FLUX.1-schnell",
1153
- transformer=transformer,
1154
- torch_dtype=torch.bfloat16,
1155
- )
1156
-
1157
- pipe.enable_model_cpu_offload()
1158
-
1159
- # --- GENERATION ---
1160
- print(f"🎨 Generating {len(PROMPTS)} images...")
1161
-
1162
- for i, prompt in enumerate(PROMPTS, 1):
1163
- print(f" Frame {i}/{len(PROMPTS)}...")
1164
- gc.collect()
1165
- torch.cuda.empty_cache()
1166
-
1167
- # Generate
1168
- image = pipe(
1169
- prompt=prompt,
1170
- width=1072,
1171
- height=1920,
1172
- num_inference_steps=4, # Schnell is fast
1173
- guidance_scale=1.0,
1174
- max_sequence_length=512,
1175
- ).images[0]
1176
-
1177
- # Save
1178
- save_path = OUTPUT_DIR / f"{i:02d}.png"
1179
- image.save(save_path)
1180
- print(f" ✅ Saved: {save_path.name}")
1181
-
1182
- print("🏁 Job Complete")
1183
- ```
1184
-
1185
- ### kaggle_tts_stt_template.py
1186
- ```py
1187
- # kaggle_tts_stt_template.py
1188
- # StyleTTS2 (LibriTTS, full quality) + Whisper large-v3 (float32, no quantization)
1189
- # Optimised for single P100 GPU / 16 GB VRAM
1190
- # Output: /kaggle/working/voice.wav + /kaggle/working/words.json
1191
-
1192
- import os, json, re, gc, subprocess, sys
1193
- from pathlib import Path
1194
- import torch
1195
- import numpy as np
1196
-
1197
- OUTPUT_DIR = Path("/kaggle/working")
1198
- SCRIPT_TEXT = {{SCRIPT_TEXT_PLACEHOLDER}}
1199
-
1200
- # ── System deps ───────────────────────────────────────────────────────────────
1201
- print("📦 System deps...")
1202
- subprocess.check_call(["apt-get","install","-y","-q","espeak-ng","libsndfile1"],
1203
- stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
1204
-
1205
- # ── Python deps ───────────────────────────────────────────────────────────────
1206
- print("📦 Installing styletts2 + phonemizer + soundfile...")
1207
- subprocess.check_call([sys.executable,"-m","pip","install","-q",
1208
- "styletts2","phonemizer","gruut","soundfile"])
1209
-
1210
- # ── CRITICAL: restore numpy 2.x ───────────────────────────────────────────────
1211
- # styletts2 pip solver downgrades numpy to 1.26.x but Kaggle's pre-compiled
1212
- # pandas/bigquery/kaggle_gcp require numpy 2.x — causes binary incompatibility crash.
1213
- # Fix: force reinstall numpy 2.x BEFORE any google/pandas import.
1214
- print("🔧 Restoring numpy>=2.0...")
1215
- subprocess.check_call([sys.executable,"-m","pip","install","-q",
1216
- "--upgrade","--force-reinstall","--no-deps","numpy>=2.0.0"])
1217
-
1218
- print("📦 Installing openai-whisper...")
1219
- subprocess.check_call([sys.executable,"-m","pip","install","-q","openai-whisper"])
1220
-
1221
- import importlib
1222
- numpy = importlib.import_module("numpy")
1223
- print(f"✅ numpy {numpy.__version__}")
1224
-
1225
- # ── Prepare script ────────────────────────────────────────────────────────────
1226
- clean = re.sub(r'\[.*?\]','',SCRIPT_TEXT).strip()
1227
- clean = re.sub(r'\s+',' ',clean)
1228
- print(f"\n📝 Script: {len(clean)} chars")
1229
- if not clean: raise RuntimeError("Empty script")
1230
-
1231
- # ── StyleTTS2 TTS ─────────────────────────────────────────────────────────────
1232
- print("\n🎙️ StyleTTS2...")
1233
- from styletts2.tts import StyleTTS2
1234
- import soundfile as sf
1235
-
1236
- tts = StyleTTS2()
1237
- wav = tts.inference(text=clean,alpha=0.3,beta=0.7,diffusion_steps=10,embedding_scale=1)
1238
- wav_np = numpy.array(wav,dtype=numpy.float32)
1239
- sf.write(str(OUTPUT_DIR/"voice.wav"),wav_np,24000)
1240
- dur = len(wav_np)/24000
1241
- print(f"✅ voice.wav ({dur:.1f}s)")
1242
- if dur < 5: raise RuntimeError(f"voice.wav only {dur:.1f}s — TTS failed")
1243
-
1244
- del tts,wav,wav_np; gc.collect(); torch.cuda.empty_cache()
1245
- print("🧹 VRAM cleared")
1246
-
1247
- # ── Whisper large-v3 (fp32, full quality) ─────────────────────────────────────
1248
- print("\n📝 Whisper large-v3 (fp32)...")
1249
- import whisper
1250
-
1251
- model = whisper.load_model("large-v3",device="cuda")
1252
- result = model.transcribe(str(OUTPUT_DIR/"voice.wav"),language="en",
1253
- word_timestamps=True,verbose=False,
1254
- beam_size=5,best_of=5,temperature=0.0,fp16=False)
1255
-
1256
- words = []
1257
- for seg in result["segments"]:
1258
- for w in seg.get("words",[]):
1259
- t = w["word"].strip()
1260
- if t: words.append({"text":t,"start":round(float(w["start"]),3),"end":round(float(w["end"]),3)})
1261
-
1262
- if not words: raise RuntimeError("Whisper returned 0 words")
1263
- (OUTPUT_DIR/"words.json").write_text(json.dumps(words,indent=2))
1264
- print(f"✅ words.json ({len(words)} words)")
1265
- print("\n🏁 TTS+STT Complete")
1266
-
1267
- ```
1268
-
1269
- ## MANUAL STEPS REQUIRED AFTER PUSH
1270
-
1271
- 1. Set Space secrets at https://huggingface.co/spaces/arshitmalik/yt-openclaw/settings
1272
- OPENCLAW_PASSWORD=arshit2025
1273
- HF_TOKEN=hf_REDACTED_SET_IN_HF_SECRETS
1274
- OPENCLAW_DATASET_REPO=arshitmalik/yt-pipeline-data
1275
- KAGGLE_USERNAME=arshitmalik
1276
- KAGGLE_KEY=REDACTED_SET_IN_KAGGLE_KEYS_JSON
1277
- YOUTUBE_CLIENT_ID=(from Google Cloud Console)
1278
- YOUTUBE_CLIENT_SECRET=(from Google Cloud Console)
1279
- TELEGRAM_TOKEN=(from @BotFather)
1280
-
1281
- 2. Run auth_youtube.py on Mac, upload token.pickle to arshitmalik/yt-pipeline-data dataset
1282
- 3. Go to Space URL, enter password arshit2025, configure Telegram channel in sidebar
1283
- 4. Set up UptimeRobot: ping https://arshitmalik-yt-openclaw.hf.space every 5 min
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
client_secrets.json DELETED
@@ -1 +0,0 @@
1
- {"installed": {"client_id": "945316832951-u78qvtfc3239hhp5tgv9pb6fqdrlujko.apps.googleusercontent.com", "project_id": "yt-ai-bot", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "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", "redirect_uris": ["http://localhost"]}}
 
 
token.pickle DELETED
Binary file (1.04 kB)