@danhachuel/thunderbolt 0.2.13

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.
@@ -0,0 +1,220 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import tempfile
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+ STORAGE = Path(os.getenv("THUNDERBOLT_STORAGE_DIR") or ROOT / "storage")
13
+ STATE = STORAGE / "state"
14
+ BLUEPRINTS = STORAGE / "blueprints"
15
+ SEED_BLUEPRINTS = ROOT / "seed" / "blueprints"
16
+
17
+ DEFAULTS: dict[str, Any] = {
18
+ "channels.json": [],
19
+ "tasks.json": [],
20
+ "queues.json": {"niche": [], "blueprint": [], "brand": [], "script": [], "title": [], "thumbnail": [], "video": [], "edit": [], "upload": []},
21
+ "batches.json": [],
22
+ "uploads.json": [],
23
+ "metadata_edits.json": [],
24
+ "settings.json": {
25
+ "port": 3030,
26
+ "moneyprinter_path": "",
27
+ "script_interval_minutes": 10,
28
+ "llm_rpm_limit": 40,
29
+ "video_concurrency": 3,
30
+ "upload_concurrency": 2,
31
+ "youtube_api_key": "",
32
+ "llm_provider": "moonshot",
33
+ "moonshot_api_key": "",
34
+ "moonshot_base_url": "",
35
+ "moonshot_model_name": "",
36
+ "shengsuanyun_api_key": "",
37
+ "shengsuanyun_base_url": "",
38
+ "shengsuanyun_model_name": "",
39
+ "openai_api_key": "",
40
+ "openai_base_url": "",
41
+ "openai_model_name": "",
42
+ "gemini_api_key": "",
43
+ "gemini_model_name": "",
44
+ "deepseek_api_key": "",
45
+ "deepseek_base_url": "",
46
+ "deepseek_model_name": "",
47
+ "qwen_api_key": "",
48
+ "qwen_model_name": "",
49
+ "azure_api_key": "",
50
+ "azure_base_url": "",
51
+ "azure_model_name": "",
52
+ "azure_api_version": "2024-02-15-preview",
53
+ "volcengine_api_key": "",
54
+ "volcengine_base_url": "",
55
+ "volcengine_model_name": "",
56
+ "grok_api_key": "",
57
+ "grok_base_url": "",
58
+ "grok_model_name": "",
59
+ "minimax_api_key": "",
60
+ "minimax_base_url": "",
61
+ "minimax_model_name": "",
62
+ "mimo_api_key": "",
63
+ "mimo_base_url": "",
64
+ "mimo_model_name": "",
65
+ "cloudflare_api_key": "",
66
+ "cloudflare_account_id": "",
67
+ "cloudflare_gateway_id": "",
68
+ "cloudflare_model_name": "",
69
+ "modelscope_api_key": "",
70
+ "modelscope_base_url": "",
71
+ "modelscope_model_name": "",
72
+ "aihubmix_api_key": "",
73
+ "aihubmix_base_url": "",
74
+ "aihubmix_model_name": "",
75
+ "aimlapi_api_key": "",
76
+ "aimlapi_base_url": "",
77
+ "aimlapi_model_name": "",
78
+ "evolink_api_key": "",
79
+ "evolink_base_url": "",
80
+ "evolink_model_name": "",
81
+ "ollama_base_url": "",
82
+ "ollama_model_name": "",
83
+ "oneapi_api_key": "",
84
+ "oneapi_base_url": "",
85
+ "oneapi_model_name": "",
86
+ "litellm_model_name": "",
87
+ "groq_api_key": "",
88
+ "groq_base_url": "",
89
+ "groq_model_name": "",
90
+ "pollinations_api_key": "",
91
+ "pollinations_base_url": "",
92
+ "pollinations_model_name": "",
93
+ "log_level": "DEBUG",
94
+ "listen_host": "127.0.0.1",
95
+ "listen_port": 8080,
96
+ "video_source": "pexels",
97
+ "match_materials_to_script": False,
98
+ "endpoint": "",
99
+ "proxy_http": "",
100
+ "proxy_https": "",
101
+ "pexels_api_keys": "",
102
+ "pixabay_api_keys": "",
103
+ "coverr_api_keys": "",
104
+ "twelvelabs_api_keys": "",
105
+ "sonilo_api_key": "",
106
+ "subtitle_provider": "edge",
107
+ "ffmpeg_path": "",
108
+ "video_codec": "",
109
+ "material_directory": "",
110
+ "whisper_model_size": "large-v3",
111
+ "whisper_device": "cpu",
112
+ "whisper_compute_type": "int8",
113
+ "azure_speech_key": "",
114
+ "azure_speech_region": "",
115
+ "siliconflow_tts_api_key": "",
116
+ "minimax_tts_api_key": "",
117
+ "minimax_tts_base_url": "",
118
+ "minimax_tts_model_id": "speech-2.8-hd",
119
+ "minimax_tts_voice_id": "English_expressive_narrator",
120
+ "elevenlabs_api_key": "",
121
+ "elevenlabs_model_id": "eleven_multilingual_v2",
122
+ "chatterbox_base_url": "http://127.0.0.1:4123/v1",
123
+ "chatterbox_api_key": "",
124
+ "chatterbox_model_id": "chatterbox",
125
+ "upload_post_enabled": False,
126
+ "upload_post_api_key": "",
127
+ "upload_post_username": "",
128
+ "upload_post_platforms": "tiktok,instagram",
129
+ "upload_post_auto_upload": False,
130
+ "tiktok_client_key": "",
131
+ "tiktok_client_secret": "",
132
+ "tiktok_redirect_uri": "http://localhost:3030/oauth/tiktok/callback",
133
+ "tiktok_scopes": "user.info.basic,video.publish,video.upload",
134
+ "tiktok_access_token": "",
135
+ "tiktok_connection_status": "not_configured",
136
+ },
137
+ }
138
+
139
+
140
+ def now() -> str:
141
+ return datetime.now(timezone.utc).isoformat()
142
+
143
+
144
+ def seed_blueprints() -> None:
145
+ """Copy packaged seed Blueprints without overwriting local user files."""
146
+ if not SEED_BLUEPRINTS.exists():
147
+ return
148
+ destination = BLUEPRINTS / "importados"
149
+ destination.mkdir(parents=True, exist_ok=True)
150
+ for source in sorted(SEED_BLUEPRINTS.glob("*.json")):
151
+ target = destination / source.name
152
+ if not target.exists():
153
+ shutil.copy2(source, target)
154
+
155
+
156
+ def ensure_storage() -> None:
157
+ for path in [STATE, BLUEPRINTS / "canais", BLUEPRINTS / "nichos", BLUEPRINTS / "importados", BLUEPRINTS / "brandings", STORAGE / "brand", STORAGE / "scripts", STORAGE / "thumbnails", STORAGE / "videos", STORAGE / "artifacts", STORAGE / "metadata_cleaner", STORAGE / "metadata_cleaner" / "outputs"]:
158
+ path.mkdir(parents=True, exist_ok=True)
159
+ seed_blueprints()
160
+ for filename, default in DEFAULTS.items():
161
+ target = STATE / filename
162
+ if not target.exists():
163
+ atomic_write(target, default)
164
+
165
+
166
+ def atomic_write(path: Path, data: Any) -> None:
167
+ path.parent.mkdir(parents=True, exist_ok=True)
168
+ fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
169
+ try:
170
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
171
+ json.dump(data, handle, ensure_ascii=False, indent=2)
172
+ handle.write("\n")
173
+ handle.flush()
174
+ os.fsync(handle.fileno())
175
+ os.replace(temp_name, path)
176
+ finally:
177
+ if os.path.exists(temp_name):
178
+ os.unlink(temp_name)
179
+
180
+
181
+ def read_json(name: str, default: Any | None = None) -> Any:
182
+ ensure_storage()
183
+ path = STATE / name
184
+ try:
185
+ with path.open("r", encoding="utf-8") as handle:
186
+ return json.load(handle)
187
+ except (json.JSONDecodeError, OSError):
188
+ backup = path.with_suffix(path.suffix + f".corrupt-{datetime.now().strftime('%Y%m%d%H%M%S')}")
189
+ if path.exists():
190
+ shutil.copy2(path, backup)
191
+ fallback = DEFAULTS.get(name, [] if default is None else default)
192
+ atomic_write(path, fallback)
193
+ return fallback
194
+
195
+
196
+ def write_json(name: str, data: Any) -> None:
197
+ ensure_storage()
198
+ atomic_write(STATE / name, data)
199
+
200
+
201
+ def append_json(name: str, item: dict[str, Any]) -> dict[str, Any]:
202
+ entries = read_json(name, [])
203
+ if not isinstance(entries, list):
204
+ entries = []
205
+ entries.append(item)
206
+ write_json(name, entries)
207
+ return item
208
+
209
+
210
+ def list_blueprint_files() -> list[Path]:
211
+ ensure_storage()
212
+ return sorted(BLUEPRINTS.rglob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
213
+
214
+
215
+ def load_blueprint_file(path: Path) -> dict[str, Any]:
216
+ with path.open("r", encoding="utf-8") as handle:
217
+ data = json.load(handle)
218
+ if not isinstance(data, dict):
219
+ raise ValueError("O blueprint deve ser um objecto JSON.")
220
+ return data
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ class MoneyPrinterRuntime:
9
+ """Detecta apenas a instalação local do MoneyPrinterTurbo."""
10
+
11
+ def __init__(self, settings: dict[str, Any]):
12
+ self.settings = settings
13
+ self.moneyprinter_path = Path(settings.get("moneyprinter_path", "") or os.getenv("MONEYPRINTER_PATH", ""))
14
+
15
+ def moneyprinter_available(self) -> bool:
16
+ return bool(self.moneyprinter_path and self.moneyprinter_path.exists())
17
+
18
+ def status(self) -> dict[str, Any]:
19
+ available = self.moneyprinter_available()
20
+ return {
21
+ "moneyprinter": available,
22
+ "mode": "moneyprinter" if available else "not_configured",
23
+ "moneyprinter_path": str(self.moneyprinter_path) if self.moneyprinter_path else "",
24
+ }
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ try:
7
+ import toml
8
+ except ImportError: # pragma: no cover - installation fallback
9
+ toml = None
10
+
11
+
12
+ def _list_value(value: Any) -> list[str]:
13
+ if isinstance(value, list):
14
+ return [str(item).strip() for item in value if str(item).strip()]
15
+ return [item.strip() for item in str(value or "").replace("\n", ",").split(",") if item.strip()]
16
+
17
+
18
+ def _set_if_present(target: dict[str, Any], key: str, value: Any) -> None:
19
+ if value is not None:
20
+ target[key] = value
21
+
22
+
23
+ def build_moneyprinter_config(settings: dict[str, Any], existing: dict[str, Any] | None = None) -> dict[str, Any]:
24
+ config = dict(existing or {})
25
+ app = dict(config.get("app") or {})
26
+ whisper = dict(config.get("whisper") or {})
27
+ azure = dict(config.get("azure") or {})
28
+ siliconflow = dict(config.get("siliconflow") or {})
29
+ minimax_tts = dict(config.get("minimax_tts") or {})
30
+ elevenlabs = dict(config.get("elevenlabs") or {})
31
+ chatterbox = dict(config.get("chatterbox") or {})
32
+ proxy = dict(config.get("proxy") or {})
33
+
34
+ app_map = {
35
+ "llm_provider": "llm_provider",
36
+ "moonshot_api_key": "moonshot_api_key",
37
+ "moonshot_base_url": "moonshot_base_url",
38
+ "moonshot_model_name": "moonshot_model_name",
39
+ "shengsuanyun_api_key": "shengsuanyun_api_key",
40
+ "shengsuanyun_base_url": "shengsuanyun_base_url",
41
+ "shengsuanyun_model_name": "shengsuanyun_model_name",
42
+ "openai_api_key": "openai_api_key",
43
+ "openai_base_url": "openai_base_url",
44
+ "openai_model_name": "openai_model_name",
45
+ "gemini_api_key": "gemini_api_key",
46
+ "gemini_model_name": "gemini_model_name",
47
+ "deepseek_api_key": "deepseek_api_key",
48
+ "deepseek_base_url": "deepseek_base_url",
49
+ "deepseek_model_name": "deepseek_model_name",
50
+ "qwen_api_key": "qwen_api_key",
51
+ "qwen_model_name": "qwen_model_name",
52
+ "azure_api_key": "azure_api_key",
53
+ "azure_base_url": "azure_base_url",
54
+ "azure_model_name": "azure_model_name",
55
+ "azure_api_version": "azure_api_version",
56
+ "volcengine_api_key": "volcengine_api_key",
57
+ "volcengine_base_url": "volcengine_base_url",
58
+ "volcengine_model_name": "volcengine_model_name",
59
+ "grok_api_key": "grok_api_key",
60
+ "grok_base_url": "grok_base_url",
61
+ "grok_model_name": "grok_model_name",
62
+ "minimax_api_key": "minimax_api_key",
63
+ "minimax_base_url": "minimax_base_url",
64
+ "minimax_model_name": "minimax_model_name",
65
+ "mimo_api_key": "mimo_api_key",
66
+ "mimo_base_url": "mimo_base_url",
67
+ "mimo_model_name": "mimo_model_name",
68
+ "cloudflare_api_key": "cloudflare_api_key",
69
+ "cloudflare_account_id": "cloudflare_account_id",
70
+ "cloudflare_gateway_id": "cloudflare_gateway_id",
71
+ "cloudflare_model_name": "cloudflare_model_name",
72
+ "modelscope_api_key": "modelscope_api_key",
73
+ "modelscope_base_url": "modelscope_base_url",
74
+ "modelscope_model_name": "modelscope_model_name",
75
+ "aihubmix_api_key": "aihubmix_api_key",
76
+ "aihubmix_base_url": "aihubmix_base_url",
77
+ "aihubmix_model_name": "aihubmix_model_name",
78
+ "aimlapi_api_key": "aimlapi_api_key",
79
+ "aimlapi_base_url": "aimlapi_base_url",
80
+ "aimlapi_model_name": "aimlapi_model_name",
81
+ "evolink_api_key": "evolink_api_key",
82
+ "evolink_base_url": "evolink_base_url",
83
+ "evolink_model_name": "evolink_model_name",
84
+ "ollama_base_url": "ollama_base_url",
85
+ "ollama_model_name": "ollama_model_name",
86
+ "oneapi_api_key": "oneapi_api_key",
87
+ "oneapi_base_url": "oneapi_base_url",
88
+ "oneapi_model_name": "oneapi_model_name",
89
+ "litellm_model_name": "litellm_model_name",
90
+ "groq_api_key": "groq_api_key",
91
+ "groq_base_url": "groq_base_url",
92
+ "groq_model_name": "groq_model_name",
93
+ "pollinations_api_key": "pollinations_api_key",
94
+ "pollinations_base_url": "pollinations_base_url",
95
+ "pollinations_model_name": "pollinations_model_name",
96
+ "log_level": "log_level",
97
+ "listen_host": "listen_host",
98
+ "listen_port": "listen_port",
99
+ "video_source": "video_source",
100
+ "endpoint": "endpoint",
101
+ "material_directory": "material_directory",
102
+ "match_materials_to_script": "match_materials_to_script",
103
+ "sonilo_api_key": "sonilo_api_key",
104
+ "sonilo_base_url": "sonilo_base_url",
105
+ "subtitle_provider": "subtitle_provider",
106
+ "ffmpeg_path": "ffmpeg_path",
107
+ "video_codec": "video_codec",
108
+ "material_directory": "material_directory",
109
+ "match_materials_to_script": "match_materials_to_script",
110
+ "twelvelabs_rerank_terms": "twelvelabs_rerank_terms",
111
+ }
112
+ for settings_key, config_key in app_map.items():
113
+ if settings_key in settings:
114
+ app[config_key] = settings[settings_key]
115
+ for key in ("pexels_api_keys", "pixabay_api_keys", "coverr_api_keys", "twelvelabs_api_keys"):
116
+ if key in settings:
117
+ app[key] = _list_value(settings[key])
118
+ config["app"] = app
119
+
120
+ whisper["model_size"] = settings.get("whisper_model_size", whisper.get("model_size", "large-v3"))
121
+ whisper["device"] = settings.get("whisper_device", whisper.get("device", "cpu"))
122
+ whisper["compute_type"] = settings.get("whisper_compute_type", whisper.get("compute_type", "int8"))
123
+ config["whisper"] = whisper
124
+
125
+ azure["speech_key"] = settings.get("azure_speech_key", azure.get("speech_key", ""))
126
+ azure["speech_region"] = settings.get("azure_speech_region", azure.get("speech_region", ""))
127
+ config["azure"] = azure
128
+
129
+ siliconflow["api_key"] = settings.get("siliconflow_tts_api_key", siliconflow.get("api_key", ""))
130
+ config["siliconflow"] = siliconflow
131
+
132
+ for source, target in {
133
+ "minimax_tts_api_key": "api_key",
134
+ "minimax_tts_base_url": "base_url",
135
+ "minimax_tts_model_id": "model_id",
136
+ "minimax_tts_voice_id": "voice_id",
137
+ }.items():
138
+ if source in settings:
139
+ minimax_tts[target] = settings[source]
140
+ config["minimax_tts"] = minimax_tts
141
+
142
+ elevenlabs["api_key"] = settings.get("elevenlabs_api_key", elevenlabs.get("api_key", ""))
143
+ elevenlabs["model_id"] = settings.get("elevenlabs_model_id", elevenlabs.get("model_id", "eleven_multilingual_v2"))
144
+ config["elevenlabs"] = elevenlabs
145
+
146
+ chatterbox["base_url"] = settings.get("chatterbox_base_url", chatterbox.get("base_url", "http://127.0.0.1:4123/v1"))
147
+ chatterbox["api_key"] = settings.get("chatterbox_api_key", chatterbox.get("api_key", ""))
148
+ chatterbox["model_id"] = settings.get("chatterbox_model_id", chatterbox.get("model_id", "chatterbox"))
149
+ config["chatterbox"] = chatterbox
150
+
151
+ if settings.get("proxy_http"):
152
+ proxy["http"] = settings["proxy_http"]
153
+ if settings.get("proxy_https"):
154
+ proxy["https"] = settings["proxy_https"]
155
+ if proxy:
156
+ config["proxy"] = proxy
157
+ return config
158
+
159
+
160
+ def sync_moneyprinter_config(settings: dict[str, Any], moneyprinter_path: str) -> Path | None:
161
+ if not moneyprinter_path or toml is None:
162
+ return None
163
+ root = Path(moneyprinter_path).expanduser()
164
+ if not root.exists():
165
+ return None
166
+ target = root / "config.toml"
167
+ existing: dict[str, Any] = {}
168
+ if target.exists():
169
+ try:
170
+ existing = toml.load(target)
171
+ except Exception:
172
+ existing = {}
173
+ payload = build_moneyprinter_config(settings, existing)
174
+ target.write_text(toml.dumps(payload), encoding="utf-8")
175
+ return target
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import requests
10
+
11
+
12
+ @dataclass
13
+ class IntegrationResult:
14
+ ok: bool
15
+ message: str
16
+ data: dict[str, Any]
17
+
18
+
19
+ class YouTubeAdapter:
20
+ def __init__(self, api_key: str = ""):
21
+ self.api_key = api_key or os.getenv("YOUTUBE_API_KEY", "")
22
+
23
+ @staticmethod
24
+ def extract_channel_ref(value: str) -> str:
25
+ value = value.strip()
26
+ match = re.search(r"(?:channel/|@)([A-Za-z0-9_.-]+)", value)
27
+ return match.group(1) if match else value
28
+
29
+ def fetch_channel(self, value: str) -> IntegrationResult:
30
+ ref = self.extract_channel_ref(value)
31
+ if not self.api_key:
32
+ return IntegrationResult(False, "YOUTUBE_API_KEY não configurada; preencha os dados manualmente.", {"url": value, "handle": ref})
33
+ try:
34
+ params = {"part": "snippet,statistics", "key": self.api_key}
35
+ if ref.startswith("UC"):
36
+ params["id"] = ref
37
+ else:
38
+ params["forHandle"] = ref if ref.startswith("@") else f"@{ref}"
39
+ response = requests.get("https://www.googleapis.com/youtube/v3/channels", params=params, timeout=15)
40
+ response.raise_for_status()
41
+ items = response.json().get("items", [])
42
+ if not items:
43
+ return IntegrationResult(False, "Canal não encontrado pela API do YouTube.", {})
44
+ item = items[0]
45
+ snippet = item.get("snippet", {})
46
+ stats = item.get("statistics", {})
47
+ data = {
48
+ "youtube_id": item.get("id", ""),
49
+ "name": snippet.get("title", ""),
50
+ "handle": snippet.get("customUrl", ""),
51
+ "description": snippet.get("description", ""),
52
+ "thumbnail_url": snippet.get("thumbnails", {}).get("high", {}).get("url", ""),
53
+ "subscriber_count": int(stats["subscriberCount"]) if stats.get("subscriberCount") else None,
54
+ "video_count": int(stats["videoCount"]) if stats.get("videoCount") else None,
55
+ "view_count": int(stats["viewCount"]) if stats.get("viewCount") else None,
56
+ "metrics_source": "youtube_data_api",
57
+ }
58
+ return IntegrationResult(True, "Canal importado do YouTube.", data)
59
+ except requests.RequestException as exc:
60
+ return IntegrationResult(False, f"Falha ao consultar o YouTube: {exc}", {})
61
+
62
+
63
+ class TikTokAdapter:
64
+ def __init__(self, settings: dict[str, Any]):
65
+ self.client_key = settings.get("tiktok_client_key", "")
66
+ self.client_secret = settings.get("tiktok_client_secret", "")
67
+ # Redirect URI, scopes, OAuth e access token são geridos no TikTok for Developers Playground.
68
+ # A UI guarda apenas as credenciais da aplicação.
69
+ self.access_token = os.getenv("TIKTOK_ACCESS_TOKEN", "")
70
+
71
+ @property
72
+ def configured(self) -> bool:
73
+ return bool(self.client_key and self.client_secret)
74
+
75
+ def status(self) -> IntegrationResult:
76
+ if not self.configured:
77
+ return IntegrationResult(False, "TikTok ainda não configurado.", {"status": "not_configured"})
78
+ return IntegrationResult(True, "TikTok Client ID e Client Secret configurados; autorização e publicação são geridas no TikTok for Developers Playground.", {"status": "configured"})
79
+
80
+ def upload_video(self, video_path: str, title: str = "", privacy_level: str = "SELF_ONLY") -> IntegrationResult:
81
+ path = Path(video_path)
82
+ if not path.exists():
83
+ return IntegrationResult(False, "Ficheiro de vídeo não encontrado.", {"path": video_path})
84
+ if not self.configured:
85
+ return IntegrationResult(False, "Configure TikTok Client ID e Client Secret.", {"path": video_path})
86
+ if not self.access_token:
87
+ return IntegrationResult(False, "Conclua a autorização no TikTok for Developers Playground; o token de publicação deve ser fornecido pelo ambiente de execução.", {"path": str(path), "status": "requires_playground_authorization"})
88
+ try:
89
+ size = path.stat().st_size
90
+ payload = {"post_info": {"title": title[:2200], "privacy_level": privacy_level, "disable_duet": False, "disable_comment": False, "disable_stitch": False}, "source_info": {"source": "FILE_UPLOAD", "video_size": size, "chunk_size": size, "total_chunk_count": 1}}
91
+ response = requests.post("https://open.tiktokapis.com/v2/post/publish/video/init/", headers={"Authorization": f"Bearer {self.access_token}", "Content-Type": "application/json; charset=UTF-8"}, json=payload, timeout=30)
92
+ response.raise_for_status()
93
+ body = response.json()
94
+ error = body.get("error", {})
95
+ if error.get("code") not in (None, "ok"):
96
+ return IntegrationResult(False, f"TikTok rejeitou a inicialização: {error.get('message', error.get('code'))}", {"error": error})
97
+ data = body.get("data", {})
98
+ publish_id = data.get("publish_id", "")
99
+ upload_url = data.get("upload_url", "")
100
+ if not upload_url or not publish_id:
101
+ return IntegrationResult(False, "TikTok não devolveu upload_url e publish_id.", {"response": body})
102
+ with path.open("rb") as handle:
103
+ upload = requests.put(upload_url, headers={"Content-Type": "video/mp4", "Content-Length": str(size), "Content-Range": f"bytes 0-{size - 1}/{size}"}, data=handle, timeout=300)
104
+ upload.raise_for_status()
105
+ return IntegrationResult(True, "Vídeo enviado para processamento no TikTok.", {"publish_id": publish_id, "status": "processing"})
106
+ except requests.RequestException as exc:
107
+ return IntegrationResult(False, f"Falha no upload TikTok: {exc}", {"status": "failed"})
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@danhachuel/thunderbolt",
3
+ "version": "0.2.13",
4
+ "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
+ "main": "scripts/cli.mjs",
6
+ "type": "module",
7
+ "bin": {
8
+ "thunderbolt": "scripts/cli.mjs"
9
+ },
10
+ "files": [
11
+ "app/main.py",
12
+ "hermes_ui/*.py",
13
+ "integrations/*.py",
14
+ "scripts/*.mjs",
15
+ "storage/blueprints/**/*.json",
16
+ "seed/blueprints/**/*.json",
17
+ "README.md",
18
+ "MANUAL-INSTALACAO.md",
19
+ "requirements.txt",
20
+ "package.json"
21
+ ],
22
+ "scripts": {
23
+ "start": "node scripts/cli.mjs",
24
+ "check": "node scripts/cli.mjs --check",
25
+ "pack:check": "npm pack --dry-run"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ }
33
+ }
@@ -0,0 +1,5 @@
1
+ streamlit>=1.37,<2
2
+ requests>=2.31,<3
3
+ pandas>=2.0,<3
4
+ imageio-ffmpeg>=0.6,<1
5
+ toml>=0.10,<1