VinOS Agent commited on
Commit
2fe56f8
Β·
1 Parent(s): 7d2eaeb

VinOS Auto-Update: Strategic Sync

Browse files
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 ? 75 : 60;
102
- const subFontSize = isHook ? 40 : 36;
103
- const typeFontSize = 28;
104
 
105
- const maxTitleChars = isHook ? 20 : 26;
106
- const maxSubChars = isHook ? 38 : 44;
107
 
108
  const titleLines = this.wrapText(this.escapeXml(slide.title), maxTitleChars);
109
  const subLines = this.wrapText(this.escapeXml(slide.subtitle), maxSubChars);
110
 
111
- // Coordinates (tuned for Canva backgrounds)
112
- let currentY = 420;
113
- const titleColor = templateType === 'light' ? '#1A1A1A' : '#FFFFFF';
114
- const subColor = templateType === 'light' ? '#4A4A4A' : '#A0ABCB';
115
- const accentColor = '#E13B6B'; // VinOS Pink
 
 
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
- currentY += (isHook ? 70 : 50);
 
124
 
125
  let subTspans = subLines.map(line => {
126
  const span = `<tspan x="110" y="${currentY}">${line}</tspan>`;
127
- currentY += (subFontSize * 1.35);
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
- ${typeLabel ? `<text x="110" y="200" font-family="Arial, sans-serif" font-size="${typeFontSize}" fill="${accentColor}" font-weight="bold" letter-spacing="3">${typeLabel}</text>` : ''}
145
- <text x="970" y="200" font-family="Arial, sans-serif" font-size="28" fill="${subColor}" font-weight="bold" text-anchor="end">${pageCounter}</text>
146
-
147
- <text font-family="Arial, sans-serif" font-size="${titleFontSize}" fill="${titleColor}" font-weight="900" letter-spacing="-1">
 
 
 
 
 
 
 
148
  ${titleTspans}
149
  </text>
150
- <text font-family="Arial, sans-serif" font-size="${subFontSize}" fill="${subColor}" font-weight="400">
 
 
151
  ${subTspans}
152
  </text>
153
-
154
- <!-- Swipe Indicator -->
155
- ${i < slides.length - 1 ? `<text x="970" y="1280" font-family="Arial, sans-serif" font-size="24" fill="${accentColor}" text-anchor="end" font-weight="bold">Swipe ➑</text>` : ''}
 
 
 
 
 
 
 
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&amp;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** | `NAMA_BRAND_KAMU` |
28
- | **Handle IG** | `@handle_kamu` |
29
- | **Warna utama** | `#3B82F6` ← ganti dengan hex code warna brand kamu |
30
- | **Logo** | Inisial huruf pertama nama brand dalam lingkaran warna brand |
31
- | **Font** | Inter (weight 300–800) |
32
- | **Tone** | Casual, clean |
33
- | **Bahasa konten** | Indonesia casual, pakai "kamu" |
34
-
35
- <!--
36
- Contoh kalau brand kamu "Warung Kopi Nusantara" dengan handle @kopinusantara:
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 β€” Inter + Prinsip HIG
80
 
81
- Font yang digunakan adalah **Inter** untuk semua elemen. Hierarki visual dicapai melalui **variasi weight, size, dan opacity** β€” bukan melalui font yang berbeda. Pendekatan ini mengikuti prinsip Apple Human Interface Guidelines: satu type family, banyak ekspresi.
82
 
83
  **Import:**
84
  ```html
85
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
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 pemilik brand **ngobrol langsung** sama followers-nya. Bukan bahasa formal, bukan bahasa template β€” tapi bahasa orang yang ngerti topiknya dan cerita ke temen.
 
 
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