@danhachuel/thunderbolt 0.4.78 → 0.4.80
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/README.md +8 -0
- package/app/main.py +22 -0
- package/app/modules/niche_finder/summarizer.py +6 -1
- package/app/modules/token_optimizer/__init__.py +6 -0
- package/app/modules/token_optimizer/cache_manager.py +22 -0
- package/app/modules/token_optimizer/compressor.py +137 -0
- package/app/modules/token_optimizer/config.py +36 -0
- package/app/modules/token_optimizer/metrics.py +22 -0
- package/hermes_ui/provider_routing.py +4 -1
- package/package.json +1 -1
- package/requirements.txt +1 -0
- package/seed/blueprints/thumbnails/ANIMAIS_Thumbnail_Blueprint.md +221 -0
- package/seed/blueprints/thumbnails/CELEBRIDADES_Thumbnail_Blueprint.md +223 -0
- package/seed/blueprints/thumbnails/COCOMELON_Thumbnail_Blueprint.md +221 -0
- package/seed/blueprints/thumbnails/HISTORIA_Thumbnail_Blueprint.md +219 -0
- package/seed/blueprints/thumbnails/Lia&Chacha_Thumbnail_Blueprint.md +182 -0
- package/seed/blueprints/thumbnails/MECANICA_Thumbnail_Blueprint.md +225 -0
- package/seed/blueprints/thumbnails/NATUREZA_Thumbnail_Blueprint.md +225 -0
- package/seed/blueprints/thumbnails/ORACAO_Thumbnail_Blueprint.md +231 -0
- package/seed/blueprints/thumbnails/STICKMAN_Thumbnail_Blueprint.md +221 -0
- package/seed/blueprints/thumbnails/STOICISMO_Thumbnail_Blueprint.md +231 -0
- package/seed/blueprints/thumbnails/Thumbnail system prompt.md +98 -0
- package/seed/blueprints/thumbnails/UNIVERSO_Thumbnail_Blueprint.md +211 -0
- package/seed/blueprints/thumbnails/ZACKDF_Thumbnail_Blueprint.md +227 -0
package/README.md
CHANGED
|
@@ -443,3 +443,11 @@ Durante a execução, o backlog mostra a actividade recente do helper e o tempo
|
|
|
443
443
|
A mensagem do card de vídeo deixou de mostrar apenas os primeiros 240 caracteres. A UI apresenta um resumo de erro com até 700 caracteres e disponibiliza um expander não editável com o diagnóstico completo, incluindo os caminhos do log e do manifesto quando existem. Linhas de sucesso de Pexels, sincronização do projecto, verificação do `uv` e marcadores de caminho são excluídas do resumo terminal.
|
|
444
444
|
|
|
445
445
|
A verificação `uv sync --frozen` tem agora timeout controlado, detalhe seguro do resolver em caso de falha e confirmação explícita quando termina. O estado do manifesto é marcado como `failed` mesmo quando a falha acontece antes de a CLI do MoneyPrinterTurbo iniciar.
|
|
446
|
+
|
|
447
|
+
## Optimização nativa de tokens com jusTokenMax
|
|
448
|
+
|
|
449
|
+
O Thunderbolt integra o [jusTokenMax](https://github.com/Kalmantic/jusTokenMax) como optimizador local e reversível de contexto. Payloads JSON e transcrições muito extensas podem ser compactados antes de serem enviados aos providers LLM. Prompts criativos estruturados e conteúdos pequenos são preservados integralmente para evitar alteração semântica.
|
|
450
|
+
|
|
451
|
+
A configuração encontra-se em **Configuração API > API Keys > Optimização de tokens — jusTokenMax**. É possível activar ou desactivar o optimizador, controlar os levers por tipo, verificar a versão instalada, consultar métricas e limpar apenas os artefactos derivados. Os originais permanecem em `storage/state/token_optimizer_originals/` e podem ser recuperados pelo hash através do módulo interno.
|
|
452
|
+
|
|
453
|
+
A instalação do pacote inclui `justokenmax` a partir do repositório oficial. Se o CLI não estiver disponível ou ocorrer um erro, o Thunderbolt usa o conteúdo original e regista um fallback local; a geração não fica bloqueada. API keys, passwords, tokens e blobs base64 são redigidos antes de serem guardados em artefactos derivados.
|
package/app/main.py
CHANGED
|
@@ -44,6 +44,10 @@ from app.modules.niche_finder.apify import ApifyError, DEFAULT_ACTOR_ID, abort_a
|
|
|
44
44
|
from app.modules.niche_finder.core import NicheAnalysisError, run_niche_analysis
|
|
45
45
|
from app.modules.niche_finder.data_loader import DatasetError, download_kaggle_dataset
|
|
46
46
|
from app.modules.niche_finder.summarizer import summarize_items
|
|
47
|
+
from app.modules.token_optimizer.cache_manager import clear_derived_cache
|
|
48
|
+
from app.modules.token_optimizer.compressor import check_installation
|
|
49
|
+
from app.modules.token_optimizer.config import DEFAULTS as TOKEN_OPTIMIZER_DEFAULTS
|
|
50
|
+
from app.modules.token_optimizer.metrics import get_stats as get_token_optimizer_stats
|
|
47
51
|
from app.influencers_ui import render_ai_influencer_characters, render_ai_influencer_content, render_ai_influencers_api_status, render_motion_control, render_ugc_products
|
|
48
52
|
from hermes_ui.blueprints import create_blueprint_from_link, list_branding_files, save_generated_blueprint
|
|
49
53
|
from hermes_ui.thumbnail_blueprints import generate_thumbnail_blueprint, list_thumbnail_blueprint_documents, resolve_thumbnail_blueprint, save_thumbnail_blueprint, save_thumbnail_blueprint_pair, thumbnail_blueprint_catalog, thumbnail_blueprint_for_blueprint, thumbnail_blueprint_for_channel
|
|
@@ -7133,6 +7137,24 @@ def render_settings():
|
|
|
7133
7137
|
st.subheader("API Keys")
|
|
7134
7138
|
moneyprinter_path = str(settings.get("moneyprinter_path") or "").strip()
|
|
7135
7139
|
st.caption(f"Pasta do motor de vídeo: `{moneyprinter_path or 'não configurada'}`")
|
|
7140
|
+
with st.expander("Optimização de tokens — jusTokenMax", expanded=False):
|
|
7141
|
+
st.caption("Compressão local e reversível de contextos volumosos antes das chamadas LLM. Os originais permanecem guardados no storage local.")
|
|
7142
|
+
optimizer_enabled = st.checkbox("Activar optimizador", value=bool(settings.get("token_optimizer_enabled", True)), key="token_optimizer_enabled_ui")
|
|
7143
|
+
optimizer_cols = st.columns(4)
|
|
7144
|
+
optimizer_values = {}
|
|
7145
|
+
for index, (key, label) in enumerate((("json", "JSON/API"), ("log", "Logs"), ("pdf", "PDF"), ("csv", "CSV"), ("diff", "Diffs"), ("code", "Código"))):
|
|
7146
|
+
with optimizer_cols[index % 4]:
|
|
7147
|
+
optimizer_values[f"token_optimizer_{key}_enabled"] = st.checkbox(label, value=bool(settings.get(f"token_optimizer_{key}_enabled", True)), key=f"token_optimizer_{key}_ui")
|
|
7148
|
+
if st.button("Guardar optimizador", key="save_token_optimizer", use_container_width=True):
|
|
7149
|
+
settings.update({"token_optimizer_enabled": optimizer_enabled, **optimizer_values})
|
|
7150
|
+
write_json("settings.json", settings)
|
|
7151
|
+
st.success("Configuração do jusTokenMax guardada.")
|
|
7152
|
+
status = check_installation()
|
|
7153
|
+
st.caption(f"Estado: {'instalado' if status.get('installed') else 'não instalado'}{(' · versão ' + str(status.get('version'))) if status.get('version') else ''}")
|
|
7154
|
+
stats = get_token_optimizer_stats()
|
|
7155
|
+
st.caption(f"Contextos: {stats['calls']} · redução acumulada: {stats['reduction_percent']}% · fallbacks: {stats['fallbacks']}")
|
|
7156
|
+
if st.button("Limpar cache derivado", key="clear_token_optimizer_cache"):
|
|
7157
|
+
st.info(f"{clear_derived_cache()} artefacto(s) derivado(s) removido(s). Os originais da aplicação não foram apagados.")
|
|
7136
7158
|
with st.form("settings_form"):
|
|
7137
7159
|
with st.expander("Niche Finder — Kaggle", expanded=False):
|
|
7138
7160
|
st.caption("O dataset permanece no Kaggle. O Thunderbolt usa estas credenciais apenas para publicar/executar a kernel e obter os resultados pequenos da análise.")
|
|
@@ -4,6 +4,8 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
import requests
|
|
6
6
|
|
|
7
|
+
from app.modules.token_optimizer import compress_text
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
DEFAULT_BASE_URLS = {
|
|
9
11
|
"openai": "https://api.openai.com/v1",
|
|
@@ -54,13 +56,16 @@ def summarize_transcript(transcript: str, settings: dict[str, Any], *, timeout:
|
|
|
54
56
|
"esta estrutura: INTRODUÇÃO: um parágrafo sobre como o vídeo começa. ESTRUTURA: uma lista com "
|
|
55
57
|
"os pontos principais e subpontos da progressão do vídeo."
|
|
56
58
|
)
|
|
59
|
+
prepared_transcript = transcript
|
|
60
|
+
if len(transcript) >= 50000:
|
|
61
|
+
prepared_transcript = compress_text(transcript, "log", settings=settings).content
|
|
57
62
|
payload = {
|
|
58
63
|
"model": model,
|
|
59
64
|
"temperature": 0.2,
|
|
60
65
|
"max_tokens": 1200,
|
|
61
66
|
"messages": [
|
|
62
67
|
{"role": "system", "content": system_prompt},
|
|
63
|
-
{"role": "user", "content":
|
|
68
|
+
{"role": "user", "content": prepared_transcript[:120000]},
|
|
64
69
|
],
|
|
65
70
|
}
|
|
66
71
|
try:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Native jusTokenMax integration for Thunderbolt LLM contexts."""
|
|
2
|
+
|
|
3
|
+
from .compressor import TokenOptimizationResult, compress_text, compress_payload, check_installation, retrieve_original
|
|
4
|
+
from .metrics import get_stats
|
|
5
|
+
|
|
6
|
+
__all__ = ["TokenOptimizationResult", "compress_text", "compress_payload", "check_installation", "retrieve_original", "get_stats"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Reversible local cache management for jusTokenMax-derived contexts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from hermes_ui import storage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cache_directory() -> Path:
|
|
13
|
+
return storage.STATE / "token_optimizer_originals"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def clear_derived_cache() -> int:
|
|
17
|
+
directory = cache_directory()
|
|
18
|
+
if not directory.exists():
|
|
19
|
+
return 0
|
|
20
|
+
count = sum(1 for item in directory.iterdir() if item.is_file())
|
|
21
|
+
shutil.rmtree(directory, ignore_errors=True)
|
|
22
|
+
return count
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Safe adapter around the official jusTokenMax Python package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Mapping
|
|
11
|
+
|
|
12
|
+
from hermes_ui import storage
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class TokenOptimizationResult:
|
|
17
|
+
content: str
|
|
18
|
+
original_hash: str
|
|
19
|
+
before_chars: int
|
|
20
|
+
after_chars: int
|
|
21
|
+
kind: str
|
|
22
|
+
applied: bool
|
|
23
|
+
fallback: bool = False
|
|
24
|
+
note: str = ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _settings() -> dict[str, Any]:
|
|
28
|
+
value = storage.read_json("settings.json", {})
|
|
29
|
+
return value if isinstance(value, dict) else {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _enabled(settings: Mapping[str, Any], kind: str) -> bool:
|
|
33
|
+
if not bool(settings.get("token_optimizer_enabled", True)):
|
|
34
|
+
return False
|
|
35
|
+
return bool(settings.get(f"token_optimizer_{kind}_enabled", True))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _hash(content: str) -> str:
|
|
39
|
+
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _record(result: TokenOptimizationResult) -> None:
|
|
43
|
+
path = storage.STATE / "token_optimizer_metrics.json"
|
|
44
|
+
current = storage.read_json("token_optimizer_metrics.json", {"calls": 0, "applied": 0, "fallbacks": 0, "before_chars": 0, "after_chars": 0})
|
|
45
|
+
if not isinstance(current, dict):
|
|
46
|
+
current = {"calls": 0, "applied": 0, "fallbacks": 0, "before_chars": 0, "after_chars": 0}
|
|
47
|
+
current["calls"] = int(current.get("calls", 0)) + 1
|
|
48
|
+
current["applied"] = int(current.get("applied", 0)) + int(result.applied)
|
|
49
|
+
current["fallbacks"] = int(current.get("fallbacks", 0)) + int(result.fallback)
|
|
50
|
+
current["before_chars"] = int(current.get("before_chars", 0)) + result.before_chars
|
|
51
|
+
current["after_chars"] = int(current.get("after_chars", 0)) + result.after_chars
|
|
52
|
+
storage.write_json(path.name, current)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _cache_original(content: str, digest: str) -> None:
|
|
56
|
+
directory = storage.STATE / "token_optimizer_originals"
|
|
57
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
target = directory / f"{digest}.txt"
|
|
59
|
+
if not target.exists():
|
|
60
|
+
target.write_text(content, encoding="utf-8")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _compress_with_jus_token_max(content: str, kind: str) -> str:
|
|
64
|
+
from justokenmax.jsoncompress import compress_json
|
|
65
|
+
from justokenmax.logs import compress_log
|
|
66
|
+
from justokenmax.redact import mask_secrets
|
|
67
|
+
|
|
68
|
+
redacted, _ = mask_secrets(content)
|
|
69
|
+
redacted = re.sub(
|
|
70
|
+
r'(["\'](?:api[_-]?key|token|password|secret)["\']\s*:\s*["\'])([^"\']+)(["\'])',
|
|
71
|
+
r'\1****\3', redacted, flags=re.IGNORECASE,
|
|
72
|
+
)
|
|
73
|
+
if kind == "json":
|
|
74
|
+
compressed, _stats = compress_json(redacted)
|
|
75
|
+
return compressed
|
|
76
|
+
if kind == "log":
|
|
77
|
+
compressed, _stats = compress_log(redacted)
|
|
78
|
+
return compressed
|
|
79
|
+
return redacted
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def compress_text(text: str, content_type: str = "auto", *, settings: Mapping[str, Any] | None = None) -> TokenOptimizationResult:
|
|
83
|
+
raw = str(text or "")
|
|
84
|
+
kind = str(content_type or "auto").strip().casefold()
|
|
85
|
+
if kind == "auto":
|
|
86
|
+
try:
|
|
87
|
+
json.loads(raw)
|
|
88
|
+
kind = "json"
|
|
89
|
+
except (TypeError, ValueError):
|
|
90
|
+
kind = "text"
|
|
91
|
+
if kind not in {"json", "log"} or not _enabled(settings or _settings(), kind):
|
|
92
|
+
result = TokenOptimizationResult(raw, _hash(raw), len(raw), len(raw), kind, False, note="disabled_or_unsupported")
|
|
93
|
+
_record(result)
|
|
94
|
+
return result
|
|
95
|
+
if len(raw) < 8000:
|
|
96
|
+
result = TokenOptimizationResult(raw, _hash(raw), len(raw), len(raw), kind, False, note="below_minimum_size")
|
|
97
|
+
_record(result)
|
|
98
|
+
return result
|
|
99
|
+
digest = _hash(raw)
|
|
100
|
+
try:
|
|
101
|
+
optimized = _compress_with_jus_token_max(raw, kind)
|
|
102
|
+
if not optimized or len(optimized) >= len(raw):
|
|
103
|
+
result = TokenOptimizationResult(raw, digest, len(raw), len(raw), kind, False, note="no_reduction")
|
|
104
|
+
else:
|
|
105
|
+
_cache_original(raw, digest)
|
|
106
|
+
result = TokenOptimizationResult(optimized, digest, len(raw), len(optimized), kind, True)
|
|
107
|
+
except (ImportError, OSError, RuntimeError, TypeError, ValueError) as exc:
|
|
108
|
+
result = TokenOptimizationResult(raw, digest, len(raw), len(raw), kind, False, True, f"jusTokenMax indisponível: {type(exc).__name__}")
|
|
109
|
+
_record(result)
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def compress_payload(payload: Any, *, settings: Mapping[str, Any] | None = None) -> TokenOptimizationResult:
|
|
114
|
+
raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
|
|
115
|
+
result = compress_text(raw, "json", settings=settings)
|
|
116
|
+
if result.applied:
|
|
117
|
+
return result
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def retrieve_original(original_hash: str) -> str | None:
|
|
122
|
+
digest = str(original_hash or "").strip().lower()
|
|
123
|
+
if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest):
|
|
124
|
+
return None
|
|
125
|
+
path = storage.STATE / "token_optimizer_originals" / f"{digest}.txt"
|
|
126
|
+
try:
|
|
127
|
+
return path.read_text(encoding="utf-8")
|
|
128
|
+
except OSError:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def check_installation() -> dict[str, Any]:
|
|
133
|
+
try:
|
|
134
|
+
import importlib.metadata
|
|
135
|
+
return {"installed": True, "version": importlib.metadata.version("justokenmax")}
|
|
136
|
+
except (ImportError, importlib.metadata.PackageNotFoundError):
|
|
137
|
+
return {"installed": False, "version": "", "message": "Instale o jusTokenMax para activar a compressão nativa."}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Persistent token optimizer settings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from hermes_ui import storage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULTS = {
|
|
11
|
+
"token_optimizer_enabled": True,
|
|
12
|
+
"token_optimizer_json_enabled": True,
|
|
13
|
+
"token_optimizer_log_enabled": True,
|
|
14
|
+
"token_optimizer_pdf_enabled": True,
|
|
15
|
+
"token_optimizer_csv_enabled": True,
|
|
16
|
+
"token_optimizer_diff_enabled": True,
|
|
17
|
+
"token_optimizer_code_enabled": True,
|
|
18
|
+
"token_optimizer_redact_enabled": True,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load(settings: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
23
|
+
result = dict(DEFAULTS)
|
|
24
|
+
source = settings if isinstance(settings, dict) else storage.read_json("settings.json", {})
|
|
25
|
+
if isinstance(source, dict):
|
|
26
|
+
result.update({key: source.get(key, value) for key, value in DEFAULTS.items()})
|
|
27
|
+
return result
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def save(values: dict[str, Any]) -> dict[str, Any]:
|
|
31
|
+
current = storage.read_json("settings.json", {})
|
|
32
|
+
if not isinstance(current, dict):
|
|
33
|
+
current = {}
|
|
34
|
+
current.update({key: bool(values.get(key, default)) for key, default in DEFAULTS.items()})
|
|
35
|
+
storage.write_json("settings.json", current)
|
|
36
|
+
return current
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Local metrics for token optimization effectiveness."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from hermes_ui import storage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_stats() -> dict[str, Any]:
|
|
11
|
+
value = storage.read_json("token_optimizer_metrics.json", {})
|
|
12
|
+
stats = value if isinstance(value, dict) else {}
|
|
13
|
+
before = int(stats.get("before_chars", 0) or 0)
|
|
14
|
+
after = int(stats.get("after_chars", 0) or 0)
|
|
15
|
+
return {
|
|
16
|
+
"calls": int(stats.get("calls", 0) or 0),
|
|
17
|
+
"applied": int(stats.get("applied", 0) or 0),
|
|
18
|
+
"fallbacks": int(stats.get("fallbacks", 0) or 0),
|
|
19
|
+
"before_chars": before,
|
|
20
|
+
"after_chars": after,
|
|
21
|
+
"reduction_percent": round(max(0, before - after) * 100 / before, 2) if before else 0.0,
|
|
22
|
+
}
|
|
@@ -26,6 +26,7 @@ from .llm_providers import (
|
|
|
26
26
|
provider_definition,
|
|
27
27
|
)
|
|
28
28
|
from .storage import STORAGE, atomic_write, ensure_storage
|
|
29
|
+
from app.modules.token_optimizer import compress_text
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
POOL_LLM = "llm_text"
|
|
@@ -450,6 +451,8 @@ def route_json_request(
|
|
|
450
451
|
|
|
451
452
|
def route_llm_json(settings: Mapping[str, Any], system_prompt: str, user_prompt: str) -> RoutedResponse:
|
|
452
453
|
"""Send an OpenAI-compatible JSON chat call through the LLM pool."""
|
|
454
|
+
optimized_user_prompt = compress_text(user_prompt, "json", settings=settings) if len(str(user_prompt or "")) >= 50000 else None
|
|
455
|
+
prepared_user_prompt = optimized_user_prompt.content if optimized_user_prompt is not None else user_prompt
|
|
453
456
|
def request(card: dict[str, Any]) -> Any:
|
|
454
457
|
definition = provider_definition(card.get("provider"))
|
|
455
458
|
base_url = str(card.get("base_url") or definition.default_base_url).strip().rstrip("/")
|
|
@@ -462,7 +465,7 @@ def route_llm_json(settings: Mapping[str, Any], system_prompt: str, user_prompt:
|
|
|
462
465
|
"model": str(card.get("model") or "").strip(),
|
|
463
466
|
"messages": [
|
|
464
467
|
{"role": "system", "content": system_prompt},
|
|
465
|
-
{"role": "user", "content":
|
|
468
|
+
{"role": "user", "content": prepared_user_prompt},
|
|
466
469
|
],
|
|
467
470
|
"response_format": {"type": "json_object"},
|
|
468
471
|
}
|
package/package.json
CHANGED
package/requirements.txt
CHANGED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
🔒 STYLE LOCK — NON-NEGOTIABLE
|
|
2
|
+
|
|
3
|
+
Warm, engaging, and educational thumbnails for a channel dedicated to decoding cat behavior and feline psychology. The visual language is approachable, curiosity-driven, and emotionally resonant — conveying the mystery and charm of cats while promising clear, science-backed insights into their behavior. Thumbnails feature expressive cats in relatable everyday situations (staring, sleeping, following, ignoring), often combined with bold, curiosity-driven text that promises to reveal the "hidden meaning" behind common feline behaviors. The overall vibe is friendly, trustworthy, and slightly playful — not cold or clinical, not overly dramatic, and never cluttered or chaotic.
|
|
4
|
+
|
|
5
|
+
❌ Not allowed:
|
|
6
|
+
|
|
7
|
+
Cold, clinical, or overly scientific aesthetics (keep it warm and accessible)
|
|
8
|
+
|
|
9
|
+
Dark, gloomy, or menacing imagery (cats should look cute or intriguing, not scary)
|
|
10
|
+
|
|
11
|
+
Cluttered frames with more than 3–4 distinct visual elements
|
|
12
|
+
|
|
13
|
+
Text that exceeds 6–8 words total on the thumbnail
|
|
14
|
+
|
|
15
|
+
Aggressive "clickbait" graphics (no neon arrows, no shock expressions)
|
|
16
|
+
|
|
17
|
+
Generic stock photos of cats that don't feel personal or relatable
|
|
18
|
+
|
|
19
|
+
Overly polished or studio-style photography — keep it natural and authentic
|
|
20
|
+
|
|
21
|
+
🧍♂️ FRAMING & POSE
|
|
22
|
+
|
|
23
|
+
Primary subject: A cat (or multiple cats) displaying a specific behavior or expression — typically one of the following:
|
|
24
|
+
|
|
25
|
+
A cat staring directly at the camera — creating a sense of connection and mystery
|
|
26
|
+
|
|
27
|
+
A cat in a relatable everyday pose — sleeping, following, ignoring, kneading, zooming
|
|
28
|
+
|
|
29
|
+
A close-up of a cat's face — highlighting eyes, ears, or whiskers that communicate emotion
|
|
30
|
+
|
|
31
|
+
A cat interacting with its environment — pushing things off surfaces, staring at nothing, etc.
|
|
32
|
+
|
|
33
|
+
A cat with a human element — hands petting, a lap, or the cat observing the owner
|
|
34
|
+
|
|
35
|
+
Framing: Close-up to medium shots. The cat occupies 50–70% of the frame, filling the center or positioned slightly off-center. The cat's face and eyes are the primary focal point, creating emotional connection with the viewer.
|
|
36
|
+
|
|
37
|
+
Angle: Eye-level with the cat — creating a sense of equality and connection. Sometimes slightly below (looking up at the cat) to emphasize curiosity, or slightly above to show the cat in its environment.
|
|
38
|
+
|
|
39
|
+
Energy: Calm, curious, and slightly mysterious — the imagery conveys "your cat is trying to tell you something" and "there's more to this behavior than meets the eye."
|
|
40
|
+
|
|
41
|
+
Cropping: Generous — cats are shown with enough context to understand the situation, but close enough to see facial expressions and body language cues. Never cropped so tightly that the cat's expression becomes unclear.
|
|
42
|
+
|
|
43
|
+
🎨 BACKGROUND & LIGHTING
|
|
44
|
+
|
|
45
|
+
Environment: Warm, familiar, everyday settings — typically:
|
|
46
|
+
|
|
47
|
+
Inside a home — living rooms, bedrooms, kitchens, hallways
|
|
48
|
+
|
|
49
|
+
Cozy, natural lighting — soft window light, warm lamps, or golden-hour sunlight
|
|
50
|
+
|
|
51
|
+
Simple, uncluttered backgrounds — letting the cat stand out
|
|
52
|
+
|
|
53
|
+
Occasional outdoor settings — gardens, patios, or windowsills
|
|
54
|
+
|
|
55
|
+
Lighting: Warm, soft, natural lighting — often from a window or lamp. Creates a cozy, intimate atmosphere. No harsh studio lighting, no artificial feel.
|
|
56
|
+
|
|
57
|
+
Depth: Moderate — foreground (cat), midground (contextual elements like furniture or floor), background (room or outdoor setting). Creates a sense of being right there with the cat.
|
|
58
|
+
|
|
59
|
+
Visual noise: Low — backgrounds are clean and uncluttered, allowing the cat and any text to stand out clearly.
|
|
60
|
+
|
|
61
|
+
🧭 IDENTIFICADORES / SÍMBOLOS DO CANAL
|
|
62
|
+
|
|
63
|
+
Recurring visual symbols:
|
|
64
|
+
|
|
65
|
+
The cat itself — the channel's recurring "star" (likely a specific cat or cats that viewers recognize)
|
|
66
|
+
|
|
67
|
+
Cat body language cues — tail position, ear position, pupil dilation, whisker movement
|
|
68
|
+
|
|
69
|
+
Everyday cat behaviors — sleeping, staring, following, ignoring, kneading, zoomies
|
|
70
|
+
|
|
71
|
+
Human elements — hands, laps, furniture — showing the cat-human relationship
|
|
72
|
+
|
|
73
|
+
Simple icons — paw prints, hearts, thought bubbles (used sparingly)
|
|
74
|
+
|
|
75
|
+
Placement: The cat is the primary visual focus. Additional elements are integrated naturally into the scene — not overlaid as stickers.
|
|
76
|
+
|
|
77
|
+
Color symbolism:
|
|
78
|
+
|
|
79
|
+
Warm tones — comfort, trust, home
|
|
80
|
+
|
|
81
|
+
Soft neutrals — authenticity, simplicity
|
|
82
|
+
|
|
83
|
+
Cat's natural colors — orange, black, white, grey, calico
|
|
84
|
+
|
|
85
|
+
🚨 VISUAL ATTENTION ELEMENT
|
|
86
|
+
|
|
87
|
+
Usage: Minimal and purposeful. The channel relies on the cat's expressive face and the curiosity-driven text, not aggressive graphics.
|
|
88
|
+
|
|
89
|
+
If used:
|
|
90
|
+
|
|
91
|
+
A subtle highlight or glow around the cat's eyes or face
|
|
92
|
+
|
|
93
|
+
A gentle arrow or circle pointing to a specific behavioral detail (rare)
|
|
94
|
+
|
|
95
|
+
No emojis, no neon effects, no aggressive arrows
|
|
96
|
+
|
|
97
|
+
Rule: The "attention" comes from the cat's expression and the promise of hidden meaning — not from visual gimmicks. Keep it authentic and educational.
|
|
98
|
+
|
|
99
|
+
📝 TEXT STYLE
|
|
100
|
+
|
|
101
|
+
Structure:
|
|
102
|
+
|
|
103
|
+
Maximum 2 lines of text
|
|
104
|
+
|
|
105
|
+
Positioned prominently — often centered at the bottom, lower third, or along the side
|
|
106
|
+
|
|
107
|
+
Text occupies approximately 15–20% of the thumbnail area
|
|
108
|
+
|
|
109
|
+
Typography:
|
|
110
|
+
|
|
111
|
+
Bold, clean, sans-serif font (e.g., Montserrat, Roboto, or similar modern font) — friendly and readable
|
|
112
|
+
|
|
113
|
+
Primary text: White or light-colored, with a strong dark outline or drop shadow for contrast
|
|
114
|
+
|
|
115
|
+
Secondary text (if used): Smaller, in a contrasting warm color (orange, yellow, or white)
|
|
116
|
+
|
|
117
|
+
Hierarchy:
|
|
118
|
+
|
|
119
|
+
Line 1: Primary hook (largest, boldest) — 3–5 words max
|
|
120
|
+
|
|
121
|
+
Line 2: Supporting context (smaller) — 2–4 words max
|
|
122
|
+
|
|
123
|
+
Background for text: Often a semi-transparent dark overlay or gradient fade at the bottom of the image to ensure text readability against varied backgrounds.
|
|
124
|
+
|
|
125
|
+
🧠 TEXT PSYCHOLOGY
|
|
126
|
+
|
|
127
|
+
Tone: Curious, educational, and slightly revelatory — the text promises hidden meanings, surprising insights, and practical understanding of cat behavior. It invites the viewer to see their cat in a new light.
|
|
128
|
+
|
|
129
|
+
Common psychological triggers observed/inferred:
|
|
130
|
+
|
|
131
|
+
Curiosity/Revelation: "What Your Cat's Stare Really Means" / "The Hidden Meaning Behind..."
|
|
132
|
+
|
|
133
|
+
Relatability: "Why Your Cat Follows You Everywhere"
|
|
134
|
+
|
|
135
|
+
Surprise: "What Your Cat Is Trying to Tell You"
|
|
136
|
+
|
|
137
|
+
Practical benefit: "How to Understand Your Cat Better"
|
|
138
|
+
|
|
139
|
+
Question: "Does Your Cat Actually Love You?"
|
|
140
|
+
|
|
141
|
+
Examples of likely headline patterns:
|
|
142
|
+
|
|
143
|
+
"What Your Cat's [Behavior] Really Means"
|
|
144
|
+
|
|
145
|
+
"Why Cats [Do This Surprising Thing]"
|
|
146
|
+
|
|
147
|
+
"The Truth About [Common Cat Behavior]"
|
|
148
|
+
|
|
149
|
+
"How to Tell If Your Cat [Feels/Thinks X]"
|
|
150
|
+
|
|
151
|
+
"[Number] Signs Your Cat [Is Trying to Tell You Something]"
|
|
152
|
+
|
|
153
|
+
🎯 COMPOSITION RULES
|
|
154
|
+
|
|
155
|
+
Rule of thirds: Primary subject (cat) placed centrally or slightly off-center. Text occupies the bottom third. The background provides context in the upper portion.
|
|
156
|
+
|
|
157
|
+
Focal hierarchy:
|
|
158
|
+
|
|
159
|
+
Primary: The cat's face and expression (emotional connection)
|
|
160
|
+
|
|
161
|
+
Secondary: Text (the promise/hook)
|
|
162
|
+
|
|
163
|
+
Tertiary: Supporting visual details (environment, body language cues)
|
|
164
|
+
|
|
165
|
+
Negative space: Moderate — enough room around the cat and text to avoid clutter, but the cat fills a significant portion of the frame.
|
|
166
|
+
|
|
167
|
+
Distraction control: Maximum 3–4 distinct visual elements total (cat, text, 1–2 supporting elements like background context).
|
|
168
|
+
|
|
169
|
+
Mobile-first: Text must be large enough to read on a smartphone screen. The cat's face must be clearly visible and emotionally expressive even at small sizes.
|
|
170
|
+
|
|
171
|
+
📐 FORMAT & QUALITY
|
|
172
|
+
|
|
173
|
+
Aspect ratio:
|
|
174
|
+
16:9
|
|
175
|
+
|
|
176
|
+
Resolution:
|
|
177
|
+
1280 × 720 minimum
|
|
178
|
+
|
|
179
|
+
Image style: Natural, warm, authentic photography — not overly stylized or filtered. The imagery should feel personal, relatable, and cozy — like you're looking at your own cat. Slightly warm color grading (golden/cozy tones) is characteristic.
|
|
180
|
+
|
|
181
|
+
Sharpness: Crisp and clear — natural photography quality with good detail on the cat's face and expression. Not overly processed or artificial.
|
|
182
|
+
|
|
183
|
+
Technical restrictions: No cartoonish illustrations (unless the channel uses animated style, which is suggested by job postings). No low-quality stock photos. Imagery should feel authentic and relatable — like a cat you might know.
|
|
184
|
+
|
|
185
|
+
🧠 FINAL OBJECTIVE
|
|
186
|
+
|
|
187
|
+
Primary goal:
|
|
188
|
+
Maximize YouTube CTR for cat behavior / feline psychology / pet education content
|
|
189
|
+
|
|
190
|
+
Target emotional triggers:
|
|
191
|
+
|
|
192
|
+
Curiosity about what cats are really thinking
|
|
193
|
+
|
|
194
|
+
Desire to understand and connect with one's cat
|
|
195
|
+
|
|
196
|
+
Love and affection for cats as family members
|
|
197
|
+
|
|
198
|
+
Surprise at discovering hidden meanings in everyday behaviors
|
|
199
|
+
|
|
200
|
+
Practical desire to improve the cat-human relationship
|
|
201
|
+
|
|
202
|
+
Target audience: Cat owners, cat lovers, and anyone curious about feline psychology — from new cat parents to experienced owners who want to deepen their understanding of their feline companions.
|
|
203
|
+
|
|
204
|
+
🧾 FINAL INPUT FORMAT
|
|
205
|
+
|
|
206
|
+
VIDEO TITLE:
|
|
207
|
+
"[placeholder — will be filled in the next step with the actual video title/script]"
|
|
208
|
+
|
|
209
|
+
⚙️ FINAL SYSTEM INSTRUCTION
|
|
210
|
+
|
|
211
|
+
You are a thumbnail designer for the Felunee YouTube channel. Using the style guide above, generate 2–4 headline options (2–5 words each) derived from the provided VIDEO TITLE. Then create a thumbnail image that follows all style rules above:
|
|
212
|
+
|
|
213
|
+
Natural, warm photography (expressive cat in a relatable everyday situation)
|
|
214
|
+
|
|
215
|
+
Cozy, warm color palette — soft lighting, home environments
|
|
216
|
+
|
|
217
|
+
Bold, curiosity-driven text at the bottom (max 2 lines, clean font with outline)
|
|
218
|
+
|
|
219
|
+
1–2 supporting elements (contextual background, subtle behavioral cues)
|
|
220
|
+
|
|
221
|
+
No aggressive clickbait styling — keep it educational, warm, and curiosity-driven
|