@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.
- package/MANUAL-INSTALACAO.md +513 -0
- package/README.md +173 -0
- package/app/main.py +624 -0
- package/hermes_ui/blueprints.py +157 -0
- package/hermes_ui/domain.py +149 -0
- package/hermes_ui/metadata_cleaner.py +190 -0
- package/hermes_ui/storage.py +220 -0
- package/integrations/local_runtime.py +24 -0
- package/integrations/moneyprinter_config.py +175 -0
- package/integrations/platforms.py +107 -0
- package/package.json +33 -0
- package/requirements.txt +5 -0
- package/scripts/cli.mjs +115 -0
- package/scripts/install.mjs +315 -0
- package/seed/blueprints/BLUEPRINTCANALMILITAR.json +72 -0
- package/seed/blueprints/BlueprintAnimalFacts-Felune.json +98 -0
- package/seed/blueprints/BlueprintCocomelon.json +329 -0
- package/seed/blueprints/BlueprintFilosofiaeEstoicismo.json +350 -0
- package/seed/blueprints/BlueprintHist/303/263ria.json +337 -0
- package/seed/blueprints/BlueprintMecanicaAutomotiva-Fatosmecanicos.json +54 -0
- package/seed/blueprints/BlueprintNatureVault.json +58 -0
- package/seed/blueprints/BlueprintOra/303/247/303/243o.json +338 -0
- package/seed/blueprints/BlueprintStickMan.json +50 -0
- package/seed/blueprints/BlueprintUniverso.json +308 -0
- package/seed/blueprints/BlueprintZackD-VoxelVault.json +54 -0
- package/seed/blueprints/CELEBRITIES.json +242 -0
- package/seed/blueprints/blueprintcanalfinan/303/247as.json +329 -0
- package/storage/blueprints/importados/demo-content-hermes.json +23 -0
|
@@ -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
|
+
}
|