Spaces:
Running
Running
VinOS Agent commited on
Commit Β·
2fe56f8
1
Parent(s): 7d2eaeb
VinOS Auto-Update: Strategic Sync
Browse files- CanvaCarousel.svg +6 -22
- carousel-gen.js +46 -24
- claude-instagram-carousel-guide.md +18 -32
- skills/api_caller.js +20 -0
CanvaCarousel.svg
CHANGED
|
|
|
|
carousel-gen.js
CHANGED
|
@@ -98,33 +98,37 @@ class CarouselGen {
|
|
| 98 |
|
| 99 |
// Formatting parameters
|
| 100 |
const isHook = (slide.type || '').toLowerCase() === 'hook' || i === 0;
|
| 101 |
-
const titleFontSize = isHook ?
|
| 102 |
-
const subFontSize = isHook ?
|
| 103 |
-
const typeFontSize =
|
| 104 |
|
| 105 |
-
const maxTitleChars = isHook ?
|
| 106 |
-
const maxSubChars = isHook ?
|
| 107 |
|
| 108 |
const titleLines = this.wrapText(this.escapeXml(slide.title), maxTitleChars);
|
| 109 |
const subLines = this.wrapText(this.escapeXml(slide.subtitle), maxSubChars);
|
| 110 |
|
| 111 |
-
//
|
| 112 |
-
let currentY =
|
| 113 |
-
const titleColor = templateType === 'light' ? '#
|
| 114 |
-
const
|
| 115 |
-
const
|
|
|
|
|
|
|
| 116 |
|
|
|
|
| 117 |
let titleTspans = titleLines.map(line => {
|
| 118 |
const span = `<tspan x="110" y="${currentY}">${line}</tspan>`;
|
| 119 |
-
currentY += (titleFontSize * 1.25);
|
| 120 |
return span;
|
| 121 |
}).join('\n');
|
| 122 |
|
| 123 |
-
|
|
|
|
| 124 |
|
| 125 |
let subTspans = subLines.map(line => {
|
| 126 |
const span = `<tspan x="110" y="${currentY}">${line}</tspan>`;
|
| 127 |
-
currentY += (subFontSize * 1.
|
| 128 |
return span;
|
| 129 |
}).join('\n');
|
| 130 |
|
|
@@ -135,24 +139,42 @@ class CarouselGen {
|
|
| 135 |
let svgContent = '';
|
| 136 |
|
| 137 |
if (templateContent) {
|
| 138 |
-
// If we have template content, we inject our dynamic layer on top of it.
|
| 139 |
-
// We assume template is an <svg> tag. We append our elements before </svg>.
|
| 140 |
const closingTagIndex = templateContent.lastIndexOf('</svg>');
|
| 141 |
if (closingTagIndex !== -1) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
const dynamicLayer = `
|
| 143 |
-
<!-- Dynamic Content Layer -->
|
| 144 |
-
${
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
<text font-family="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
${titleTspans}
|
| 149 |
</text>
|
| 150 |
-
|
|
|
|
|
|
|
| 151 |
${subTspans}
|
| 152 |
</text>
|
| 153 |
-
|
| 154 |
-
<!--
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
`;
|
| 157 |
svgContent = templateContent.slice(0, closingTagIndex) + dynamicLayer + templateContent.slice(closingTagIndex);
|
| 158 |
}
|
|
|
|
| 98 |
|
| 99 |
// Formatting parameters
|
| 100 |
const isHook = (slide.type || '').toLowerCase() === 'hook' || i === 0;
|
| 101 |
+
const titleFontSize = isHook ? 88 : 72;
|
| 102 |
+
const subFontSize = isHook ? 38 : 34;
|
| 103 |
+
const typeFontSize = 26;
|
| 104 |
|
| 105 |
+
const maxTitleChars = isHook ? 18 : 24;
|
| 106 |
+
const maxSubChars = isHook ? 40 : 48;
|
| 107 |
|
| 108 |
const titleLines = this.wrapText(this.escapeXml(slide.title), maxTitleChars);
|
| 109 |
const subLines = this.wrapText(this.escapeXml(slide.subtitle), maxSubChars);
|
| 110 |
|
| 111 |
+
// Start title at a fixed Y β well below the header (TYPE + PAGE at y=200)
|
| 112 |
+
let currentY = 400;
|
| 113 |
+
const titleColor = templateType === 'light' ? '#071330' : '#FFFFFF';
|
| 114 |
+
const titleAccentColor = '#f7cb2d'; // Gold accent for hook slides
|
| 115 |
+
const subColor = templateType === 'light' ? '#3a4a6b' : '#c8d4f0';
|
| 116 |
+
const accentColor = '#f7cb2d'; // VinOS Gold
|
| 117 |
+
const bgColor = templateType === 'light' ? '#f0f3fa' : '#071330';
|
| 118 |
|
| 119 |
+
// Render title tspans β track exact final Y
|
| 120 |
let titleTspans = titleLines.map(line => {
|
| 121 |
const span = `<tspan x="110" y="${currentY}">${line}</tspan>`;
|
| 122 |
+
currentY += Math.round(titleFontSize * 1.25);
|
| 123 |
return span;
|
| 124 |
}).join('\n');
|
| 125 |
|
| 126 |
+
// Add a comfortable gap between title and subtitle
|
| 127 |
+
currentY += isHook ? 60 : 48;
|
| 128 |
|
| 129 |
let subTspans = subLines.map(line => {
|
| 130 |
const span = `<tspan x="110" y="${currentY}">${line}</tspan>`;
|
| 131 |
+
currentY += Math.round(subFontSize * 1.4);
|
| 132 |
return span;
|
| 133 |
}).join('\n');
|
| 134 |
|
|
|
|
| 139 |
let svgContent = '';
|
| 140 |
|
| 141 |
if (templateContent) {
|
|
|
|
|
|
|
| 142 |
const closingTagIndex = templateContent.lastIndexOf('</svg>');
|
| 143 |
if (closingTagIndex !== -1) {
|
| 144 |
+
const fontImport = `<defs>
|
| 145 |
+
<style>@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700;900&display=swap');</style>
|
| 146 |
+
</defs>`;
|
| 147 |
+
|
| 148 |
const dynamicLayer = `
|
| 149 |
+
<!-- Dynamic Content Layer ββ injected by carousel-gen.js -->
|
| 150 |
+
${fontImport}
|
| 151 |
+
|
| 152 |
+
<!-- Header: TYPE tag (top-left) and PAGE counter (top-right) -->
|
| 153 |
+
${typeLabel ? `<text x="110" y="185" font-family="'Space Grotesk', sans-serif" font-size="${typeFontSize}" fill="${accentColor}" font-weight="700" letter-spacing="4">${typeLabel}</text>` : ''}
|
| 154 |
+
<text x="970" y="185" font-family="'Space Grotesk', sans-serif" font-size="26" fill="${subColor}" font-weight="500" text-anchor="end" opacity="0.7">${pageCounter}</text>
|
| 155 |
+
|
| 156 |
+
<!-- Thin accent rule under header -->
|
| 157 |
+
<rect x="110" y="205" width="80" height="3" rx="2" fill="${accentColor}" />
|
| 158 |
+
|
| 159 |
+
<!-- Main TITLE β Space Grotesk Heavy -->
|
| 160 |
+
<text font-family="'Space Grotesk', sans-serif" font-size="${titleFontSize}" fill="${titleColor}" font-weight="900" letter-spacing="-1.5">
|
| 161 |
${titleTspans}
|
| 162 |
</text>
|
| 163 |
+
|
| 164 |
+
<!-- SUBTITLE β Vend Sans (falls back to system sans) -->
|
| 165 |
+
<text font-family="'Vend Sans', 'Space Grotesk', sans-serif" font-size="${subFontSize}" fill="${subColor}" font-weight="400" letter-spacing="0.2">
|
| 166 |
${subTspans}
|
| 167 |
</text>
|
| 168 |
+
|
| 169 |
+
<!-- Footer Branding -->
|
| 170 |
+
<rect x="110" y="1260" width="860" height="1" fill="${accentColor}" opacity="0.25" />
|
| 171 |
+
|
| 172 |
+
<!-- DF Logo mark in footer -->
|
| 173 |
+
<circle cx="146" cy="1305" r="26" fill="${accentColor}" />
|
| 174 |
+
<text x="146" y="1313" font-family="'Space Grotesk', sans-serif" font-size="18" fill="#071330" font-weight="900" text-anchor="middle">DF</text>
|
| 175 |
+
<text x="186" y="1313" font-family="'Space Grotesk', sans-serif" font-size="18" fill="${subColor}" font-weight="500" opacity="0.7">@deeferdinand</text>
|
| 176 |
+
|
| 177 |
+
${i < slides.length - 1 ? `<text x="970" y="1313" font-family="'Space Grotesk', sans-serif" font-size="22" fill="${accentColor}" text-anchor="end" font-weight="700" opacity="0.9">Swipe β‘</text>` : `<text x="970" y="1313" font-family="'Space Grotesk', sans-serif" font-size="22" fill="${accentColor}" text-anchor="end" font-weight="700" opacity="0.9">Follow for more</text>`}
|
| 178 |
`;
|
| 179 |
svgContent = templateContent.slice(0, closingTagIndex) + dynamicLayer + templateContent.slice(closingTagIndex);
|
| 180 |
}
|
claude-instagram-carousel-guide.md
CHANGED
|
@@ -24,21 +24,16 @@ Kalau user bilang "bikinin carousel tentang X" tanpa sebut brand lain, langsung
|
|
| 24 |
|
| 25 |
| Detail | Nilai |
|
| 26 |
|--------|-------|
|
| 27 |
-
| **Nama brand** | `
|
| 28 |
-
| **Handle IG** | `@
|
| 29 |
-
| **Warna utama** | `#
|
| 30 |
-
| **
|
| 31 |
-
| **
|
| 32 |
-
| **
|
| 33 |
-
| **
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
| Nama brand | KOPI NUSANTARA |
|
| 38 |
-
| Handle IG | @kopinusantara |
|
| 39 |
-
| Warna utama | #8B4513 |
|
| 40 |
-
| Logo | Inisial "K" dalam lingkaran coklat |
|
| 41 |
-
-->
|
| 42 |
|
| 43 |
> Kalau default sudah diisi, **gak perlu tanya lagi**. Langsung gas.
|
| 44 |
|
|
@@ -76,16 +71,15 @@ DARK_BG = {near-black dengan tint brand} // Background slide gelap
|
|
| 76 |
|
| 77 |
---
|
| 78 |
|
| 79 |
-
## Step 3: Setup Tipografi β
|
| 80 |
|
| 81 |
-
Font
|
| 82 |
|
| 83 |
**Import:**
|
| 84 |
```html
|
| 85 |
-
<link href="https://fonts.googleapis.com/css2?family=
|
| 86 |
```
|
| 87 |
-
|
| 88 |
-
> Sertakan weight **800** β dipakai khusus untuk headline cover slide.
|
| 89 |
|
| 90 |
**Skala tipografi (mengikuti 8pt grid system):**
|
| 91 |
|
|
@@ -462,20 +456,12 @@ asyncio.run(export_slides())
|
|
| 462 |
## Tone & Copywriting
|
| 463 |
|
| 464 |
<!--
|
| 465 |
-
|
| 466 |
-
β π§ SESUAIKAN BAGIAN INI DENGAN GAYA BAHASA KAMU β
|
| 467 |
-
β β
|
| 468 |
-
β Section di bawah berisi panduan tone casual bahasa Indonesia. β
|
| 469 |
-
β Kalau kamu menulis dalam bahasa Inggris atau gaya yang beda, β
|
| 470 |
-
β sesuaikan prinsip dan contoh kalimatnya. β
|
| 471 |
-
β β
|
| 472 |
-
β Tipsnya: paste beberapa contoh caption/tulisan kamu yang β
|
| 473 |
-
β sudah ada, dan minta Claude untuk analisis pola bahasanya β
|
| 474 |
-
β lalu update section ini. β
|
| 475 |
-
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 476 |
-->
|
| 477 |
|
| 478 |
-
Carousel ditulis seolah-olah
|
|
|
|
|
|
|
| 479 |
|
| 480 |
### Prinsip Tone
|
| 481 |
|
|
|
|
| 24 |
|
| 25 |
| Detail | Nilai |
|
| 26 |
|--------|-------|
|
| 27 |
+
| **Nama brand** | `@deeferdinand` |
|
| 28 |
+
| **Handle IG** | `@deeferdinand` |
|
| 29 |
+
| **Warna utama** | `#071330` (Deep Navy) |
|
| 30 |
+
| **Accent / CTA** | `#f7cb2d` (Gold) |
|
| 31 |
+
| **Contrast teks** | `#FFFFFF` di atas navy, `#071330` di atas gold |
|
| 32 |
+
| **Logo** | Inisial `DF` dalam lingkaran gold `#f7cb2d` |
|
| 33 |
+
| **Font Heading** | Space Grotesk (weight 700β900) |
|
| 34 |
+
| **Font Subheading** | Vend Sans (weight 400β600) |
|
| 35 |
+
| **Tone** | Casual, conversational, helpful, everyday |
|
| 36 |
+
| **Bahasa** | Mix Indonesia + English β pakai Inggris untuk istilah tech/bisnis yang lebih familiar |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
> Kalau default sudah diisi, **gak perlu tanya lagi**. Langsung gas.
|
| 39 |
|
|
|
|
| 71 |
|
| 72 |
---
|
| 73 |
|
| 74 |
+
## Step 3: Setup Tipografi β Space Grotesk + Vend Sans
|
| 75 |
|
| 76 |
+
Font utama adalah **Space Grotesk** untuk heading dan **Vend Sans** untuk body/subtitle. Hierarchy visual dicapai melalui **variasi weight, size, dan opacity** β prinsip Apple HIG: ekspresi maksimal dari type system yang konsisten.
|
| 77 |
|
| 78 |
**Import:**
|
| 79 |
```html
|
| 80 |
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700;900&display=swap" rel="stylesheet">
|
| 81 |
```
|
| 82 |
+
> Vend Sans belum ada di Google Fonts β embed file manual atau gunakan Space Grotesk weight 400 sebagai fallback untuk subtitle.
|
|
|
|
| 83 |
|
| 84 |
**Skala tipografi (mengikuti 8pt grid system):**
|
| 85 |
|
|
|
|
| 456 |
## Tone & Copywriting
|
| 457 |
|
| 458 |
<!--
|
| 459 |
+
Brand voice sudah terkunci untuk @deeferdinand. Aturan di bawah mencerminkan gaya komunikasi Dee.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
-->
|
| 461 |
|
| 462 |
+
Carousel ditulis seolah-olah **Dee ngobrol langsung** sama followers-nya. Bukan bahasa formal, bukan AI template β tapi bahasa orang yang ngerti topiknya dan cerita ke temen deket.
|
| 463 |
+
|
| 464 |
+
**Format bahasa: Mix Indonesia+English** β istilah tech/bisnis tetap Inggris kalau lebih familiar. Contoh: "workflow", "tools", "setup", "AI", "content calendar".
|
| 465 |
|
| 466 |
### Prinsip Tone
|
| 467 |
|
skills/api_caller.js
CHANGED
|
@@ -27,8 +27,26 @@ const getChatModel = () => {
|
|
| 27 |
}
|
| 28 |
};
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
const MODELS = {
|
| 31 |
CHAT: () => getChatModel(),
|
|
|
|
| 32 |
FALLBACK: 'meta-llama/llama-3.3-70b-instruct',
|
| 33 |
INTENT: process.env.GROQ_MODEL || 'llama-3.1-8b-instant',
|
| 34 |
IMAGE: 'google/gemini-3.1-flash-image-preview',
|
|
@@ -50,6 +68,8 @@ const logTelegramMessage = (direction, chatId, text) => {
|
|
| 50 |
module.exports = {
|
| 51 |
logTelegramMessage,
|
| 52 |
MODELS,
|
|
|
|
|
|
|
| 53 |
axiosIPv4,
|
| 54 |
|
| 55 |
// OpenRouter (AI Chat & Reasoning) β with automatic fallback
|
|
|
|
| 27 |
}
|
| 28 |
};
|
| 29 |
|
| 30 |
+
// Social content writing models (human-like, personal tone)
|
| 31 |
+
const SOCIAL_MODELS = {
|
| 32 |
+
PREMIUM: 'google/gemini-2.5-flash', // $0.30/$2.50 per 1M β hero content
|
| 33 |
+
STANDARD: 'google/gemini-2.5-flash-lite', // $0.10/$0.40 per 1M β daily driver
|
| 34 |
+
FREE: 'meta-llama/llama-3.3-70b-instruct:free' // $0.00 β budget fallback
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
const getSocialModel = () => {
|
| 38 |
+
try {
|
| 39 |
+
const raw = fs.readFileSync(DB_PATH, 'utf8');
|
| 40 |
+
const db = JSON.parse(raw);
|
| 41 |
+
return db.user_profile_snapshot?.social_content_model || SOCIAL_MODELS.STANDARD;
|
| 42 |
+
} catch (e) {
|
| 43 |
+
return SOCIAL_MODELS.STANDARD;
|
| 44 |
+
}
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
const MODELS = {
|
| 48 |
CHAT: () => getChatModel(),
|
| 49 |
+
SOCIAL: () => getSocialModel(),
|
| 50 |
FALLBACK: 'meta-llama/llama-3.3-70b-instruct',
|
| 51 |
INTENT: process.env.GROQ_MODEL || 'llama-3.1-8b-instant',
|
| 52 |
IMAGE: 'google/gemini-3.1-flash-image-preview',
|
|
|
|
| 68 |
module.exports = {
|
| 69 |
logTelegramMessage,
|
| 70 |
MODELS,
|
| 71 |
+
SOCIAL_MODELS,
|
| 72 |
+
getSocialModel,
|
| 73 |
axiosIPv4,
|
| 74 |
|
| 75 |
// OpenRouter (AI Chat & Reasoning) β with automatic fallback
|