@jjlmoya/utils-social 1.1.0

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.
Files changed (85) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +23 -0
  3. package/src/category/i18n/es.ts +23 -0
  4. package/src/category/i18n/fr.ts +23 -0
  5. package/src/category/index.ts +13 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +27 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/seo_length.test.ts +22 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/redditFormatter/bibliography.astro +14 -0
  23. package/src/tool/redditFormatter/component.astro +246 -0
  24. package/src/tool/redditFormatter/i18n/en.ts +193 -0
  25. package/src/tool/redditFormatter/i18n/es.ts +193 -0
  26. package/src/tool/redditFormatter/i18n/fr.ts +193 -0
  27. package/src/tool/redditFormatter/index.ts +34 -0
  28. package/src/tool/redditFormatter/logic.ts +24 -0
  29. package/src/tool/redditFormatter/seo.astro +14 -0
  30. package/src/tool/redditFormatter/ui.ts +13 -0
  31. package/src/tool/safeZones/bibliography.astro +14 -0
  32. package/src/tool/safeZones/component.astro +857 -0
  33. package/src/tool/safeZones/i18n/en.ts +244 -0
  34. package/src/tool/safeZones/i18n/es.ts +244 -0
  35. package/src/tool/safeZones/i18n/fr.ts +244 -0
  36. package/src/tool/safeZones/index.ts +34 -0
  37. package/src/tool/safeZones/logic.ts +137 -0
  38. package/src/tool/safeZones/seo.astro +14 -0
  39. package/src/tool/safeZones/ui.ts +34 -0
  40. package/src/tool/socialImageResizer/bibliography.astro +14 -0
  41. package/src/tool/socialImageResizer/component.astro +498 -0
  42. package/src/tool/socialImageResizer/i18n/en.ts +211 -0
  43. package/src/tool/socialImageResizer/i18n/es.ts +211 -0
  44. package/src/tool/socialImageResizer/i18n/fr.ts +211 -0
  45. package/src/tool/socialImageResizer/index.ts +34 -0
  46. package/src/tool/socialImageResizer/logic.ts +133 -0
  47. package/src/tool/socialImageResizer/seo.astro +14 -0
  48. package/src/tool/socialImageResizer/ui.ts +7 -0
  49. package/src/tool/socialValueCalculator/bibliography.astro +14 -0
  50. package/src/tool/socialValueCalculator/component.astro +829 -0
  51. package/src/tool/socialValueCalculator/i18n/en.ts +310 -0
  52. package/src/tool/socialValueCalculator/i18n/es.ts +310 -0
  53. package/src/tool/socialValueCalculator/i18n/fr.ts +310 -0
  54. package/src/tool/socialValueCalculator/index.ts +34 -0
  55. package/src/tool/socialValueCalculator/logic.ts +60 -0
  56. package/src/tool/socialValueCalculator/seo.astro +14 -0
  57. package/src/tool/socialValueCalculator/ui.ts +31 -0
  58. package/src/tool/tinderPhotoOptimizer/bibliography.astro +14 -0
  59. package/src/tool/tinderPhotoOptimizer/component.astro +1514 -0
  60. package/src/tool/tinderPhotoOptimizer/i18n/en.ts +248 -0
  61. package/src/tool/tinderPhotoOptimizer/i18n/es.ts +248 -0
  62. package/src/tool/tinderPhotoOptimizer/i18n/fr.ts +248 -0
  63. package/src/tool/tinderPhotoOptimizer/index.ts +34 -0
  64. package/src/tool/tinderPhotoOptimizer/logic.ts +199 -0
  65. package/src/tool/tinderPhotoOptimizer/seo.astro +14 -0
  66. package/src/tool/tinderPhotoOptimizer/ui.ts +37 -0
  67. package/src/tool/youtubeThumbnail/bibliography.astro +14 -0
  68. package/src/tool/youtubeThumbnail/component.astro +419 -0
  69. package/src/tool/youtubeThumbnail/i18n/en.ts +207 -0
  70. package/src/tool/youtubeThumbnail/i18n/es.ts +207 -0
  71. package/src/tool/youtubeThumbnail/i18n/fr.ts +207 -0
  72. package/src/tool/youtubeThumbnail/index.ts +34 -0
  73. package/src/tool/youtubeThumbnail/logic.ts +7 -0
  74. package/src/tool/youtubeThumbnail/seo.astro +14 -0
  75. package/src/tool/youtubeThumbnail/ui.ts +13 -0
  76. package/src/tool/youtubeThumbnailPreviewer/bibliography.astro +14 -0
  77. package/src/tool/youtubeThumbnailPreviewer/component.astro +791 -0
  78. package/src/tool/youtubeThumbnailPreviewer/i18n/en.ts +321 -0
  79. package/src/tool/youtubeThumbnailPreviewer/i18n/es.ts +321 -0
  80. package/src/tool/youtubeThumbnailPreviewer/i18n/fr.ts +321 -0
  81. package/src/tool/youtubeThumbnailPreviewer/index.ts +34 -0
  82. package/src/tool/youtubeThumbnailPreviewer/seo.astro +14 -0
  83. package/src/tool/youtubeThumbnailPreviewer/ui.ts +19 -0
  84. package/src/tools.ts +10 -0
  85. package/src/types.ts +72 -0
@@ -0,0 +1,248 @@
1
+ import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { TinderPhotoOptimizerUI } from '../ui';
4
+
5
+ const slug = 'optimiseur-photos-tinder';
6
+ const title = "Optimiseur de Photos pour Tinder : Analyse de Visage et Composition";
7
+ const description =
8
+ "Optimisez vos photos pour Tinder avec une analyse algorithmique du visage. Simule l'interface 2026, analyse le cadrage scientifique et la ligne des yeux, exporte des portraits 9:16 parfaits pour maximiser vos matchs.";
9
+
10
+ const faqData = [
11
+ {
12
+ question: "Comment cet outil améliore-t-il mes matchs sur Tinder ?",
13
+ answer:
14
+ "L'outil analyse des facteurs techniques clés : le ratio de taille du visage (idéal 30-50%), la position de la ligne des yeux par rapport au tiers supérieur, la couverture du cadre et les zones mortes de l'interface Tinder. En ajustant ces paramètres, votre photo projette plus de confiance et passe les filtres visuels de l'algorithme.",
15
+ },
16
+ {
17
+ question: "Est-il sûr de télécharger mes photos ici ?",
18
+ answer:
19
+ "Complètement sûr. Tout le traitement se fait dans votre navigateur via l'API Canvas. Vos photos ne sont jamais envoyées à un serveur ni stockées nulle part.",
20
+ },
21
+ {
22
+ question: "Que fait exactement la détection de visage par IA ?",
23
+ answer:
24
+ "Elle utilise face-api.js pour détecter 68 points de référence faciaux, analyser l'expression (sourire, air sérieux), évaluer la position des yeux et mesurer l'inclinaison de la tête. Toute l'analyse se fait localement sur votre appareil.",
25
+ },
26
+ {
27
+ question: "Pourquoi recommande-t-il un zoom à 1,2x ?",
28
+ answer:
29
+ "Les caméras frontales des smartphones utilisent des optiques grand angle (24mm) qui déforment le visage avec l'aberration en barillet. Un zoom de 1,2x simule une focale de 50 à 85mm, éliminant la distorsion et offrant des proportions faciales plus naturelles et attrayantes.",
30
+ },
31
+ ];
32
+
33
+ const howToData = [
34
+ {
35
+ name: "Téléchargez votre photo",
36
+ text: "Faites glisser ou cliquez sur le simulateur Tinder pour charger votre image.",
37
+ },
38
+ {
39
+ name: "Analysez la composition",
40
+ text: "L'IA détecte automatiquement votre visage et affiche les métriques de taille du visage, ligne des yeux, couverture et expression.",
41
+ },
42
+ {
43
+ name: "Ajustez avec précision",
44
+ text: "Utilisez les contrôles de zoom et luminosité, activez le cadrage automatique ou faites glisser manuellement l'image dans le simulateur.",
45
+ },
46
+ {
47
+ name: "Téléchargez optimisé",
48
+ text: "Exportez votre photo au format natif 1080x1920 optimisé pour Tinder, avec des métadonnées propres.",
49
+ },
50
+ ];
51
+
52
+ const faqSchema: WithContext<FAQPage> = {
53
+ '@context': 'https://schema.org',
54
+ '@type': 'FAQPage',
55
+ mainEntity: faqData.map((item) => ({
56
+ '@type': 'Question',
57
+ name: item.question,
58
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
59
+ })),
60
+ };
61
+
62
+ const howToSchema: WithContext<HowToThing> = {
63
+ '@context': 'https://schema.org',
64
+ '@type': 'HowTo',
65
+ name: title,
66
+ description,
67
+ step: howToData.map((step, i) => ({
68
+ '@type': 'HowToStep',
69
+ position: i + 1,
70
+ name: step.name,
71
+ text: step.text,
72
+ })),
73
+ };
74
+
75
+ const appSchema: WithContext<SoftwareApplication> = {
76
+ '@context': 'https://schema.org',
77
+ '@type': 'SoftwareApplication',
78
+ name: title,
79
+ description,
80
+ applicationCategory: 'UtilityApplication',
81
+ operatingSystem: 'All',
82
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
83
+ inLanguage: 'fr',
84
+ };
85
+
86
+ export const content: ToolLocaleContent<TinderPhotoOptimizerUI> = {
87
+ slug,
88
+ title,
89
+ description,
90
+ faqTitle: 'Questions Fréquentes',
91
+ faq: faqData,
92
+ bibliographyTitle: 'Références',
93
+ bibliography: [
94
+ {
95
+ name: 'face-api.js: JavaScript Face Recognition in the Browser',
96
+ url: 'https://github.com/justadudewhohacks/face-api.js',
97
+ },
98
+ {
99
+ name: 'MDN Web Docs: Canvas API',
100
+ url: 'https://developer.mozilla.org/fr/docs/Web/API/Canvas_API',
101
+ },
102
+ ],
103
+ howTo: howToData,
104
+ schemas: [faqSchema, howToSchema, appSchema],
105
+ seo: [
106
+ {
107
+ type: 'title',
108
+ text: "Optimiseur de Photos pour Tinder : Le Guide Scientifique et Technique Définitif (2026)",
109
+ level: 2,
110
+ },
111
+ {
112
+ type: 'paragraph',
113
+ html: "Dans l'écosystème des rencontres numériques, votre profil Tinder est traité par deux algorithmes simultanément : l'algorithme de vision avancée de la plateforme elle-même et le cerveau humain de l'utilisateur qui fait glisser. Notre outil intègre des principes de <strong>psychologie évolutive</strong>, de <strong>biométrie faciale</strong> et de <strong>traitement du signal numérique</strong> pour garantir que votre image projette une valeur sociale et un attrait technique maximum.",
114
+ },
115
+ {
116
+ type: 'title',
117
+ text: "La Psychologie de la Première Impression : L'Effet des 1,2 Secondes",
118
+ level: 3,
119
+ },
120
+ {
121
+ type: 'paragraph',
122
+ html: "Des études neuroscientifiques indiquent que le cerveau humain met environ 1/10 de seconde pour se former une impression initiale sur la confiance que quelqu'un projette. Sur Tinder, vous disposez d'un peu plus d'une seconde avant le prochain swipe. Notre outil analyse les facteurs qui accélèrent la prise de décision positive :",
123
+ },
124
+ {
125
+ type: 'list',
126
+ items: [
127
+ "<strong>Contact Visuel et Ligne du Regard :</strong> Le contact visuel direct active les régions du cerveau liées à la récompense sociale. Positionner les yeux dans le tiers supérieur maximise la probabilité que l'utilisateur s'arrête sur votre regard avant de lire votre nom.",
128
+ "<strong>Asymétrie et Posture :</strong> Une légère inclinaison peut adoucir des traits agressifs, mais un roll excessif transmet de l'instabilité. Notre algorithme mesure l'angle exact pour vous maintenir dans la plage de confiance.",
129
+ "<strong>Contraste Figure-Fond :</strong> Le système récompense les photos où le sujet est clairement séparé du fond. Un profil technique bien défini aide le moteur de rendu à mettre votre visage en valeur face à la concurrence.",
130
+ ],
131
+ },
132
+ {
133
+ type: 'title',
134
+ text: "Analyse Technique de l'Objectif : Le Problème du Grand Angle",
135
+ level: 3,
136
+ },
137
+ {
138
+ type: 'paragraph',
139
+ html: "Presque toutes les photos de profil sont prises avec des smartphones, dont les caméras frontales sont grand angle. L'erreur la plus courante est de se rapprocher trop de l'appareil.",
140
+ },
141
+ {
142
+ type: 'paragraph',
143
+ html: "<strong>L'Aberration en Barillet :</strong> À moins d'un mètre de distance, un objectif de 24mm (standard sur mobile) élargit le centre de l'image. Cela fait paraître les nez plus grands et le visage plus rond que dans la réalité. Notre recommandation de <strong>Zoom 1,2x</strong> est corrective : en appliquant un léger recadrage numérique, vous simulez une focale de 50mm à 85mm, la focale dorée que les photographes de mode utilisent pour affiner le visage et maintenir les proportions réelles.",
144
+ },
145
+ {
146
+ type: 'title',
147
+ text: "L'Interface Tinder : La Science des Zones Mortes",
148
+ level: 3,
149
+ },
150
+ {
151
+ type: 'paragraph',
152
+ html: "De nombreuses excellentes photos échouent parce qu'elles ignorent la couche logicielle (UI) que Tinder place par-dessus. Un utilisateur moyen ne voit pas votre photo propre ; il voit un amalgame de boutons, de dégradés et de texte.",
153
+ },
154
+ {
155
+ type: 'list',
156
+ items: [
157
+ "<strong>Zone de Titre (15% supérieur) :</strong> C'est là que se trouvent les indicateurs de progression et la barre d'état. Si votre front ou vos yeux sont là, l'interface coupe visuellement votre tête.",
158
+ "<strong>Zone d'Action (Boutons) :</strong> Les icônes Like, Nope et Boost occupent les côtés inférieurs. Si vos mains ou éléments de style sont là, ils sont totalement perdus.",
159
+ "<strong>Le Dégradé de Lisibilité (28% inférieur) :</strong> Tinder applique un fondu noir pour que votre nom et votre âge soient lisibles. Si votre visage tombe dans cette zone, le texte se superpose à vos traits, ruinant l'esthétique du profil.",
160
+ ],
161
+ },
162
+ {
163
+ type: 'title',
164
+ text: "Analyse Visuelle en Temps Réel : Décoder l'Algorithme",
165
+ level: 3,
166
+ },
167
+ {
168
+ type: 'paragraph',
169
+ html: "Notre outil utilise des <strong>modèles de Computer Vision de dernière génération</strong> pour auditer votre image comme le feraient les modérateurs automatiques de Tinder :",
170
+ },
171
+ {
172
+ type: 'list',
173
+ items: [
174
+ "<strong>Landmark Detection :</strong> Nous cartographions 68 points faciaux pour analyser la symétrie et l'ouverture des yeux. Des yeux bien ouverts et visibles se corrèlent avec des niveaux plus élevés d'extraversion perçue.",
175
+ "<strong>Mood Evaluation :</strong> Un léger sourire active les neurones miroirs chez la personne qui vous regarde. Nous détectons l'intensité de l'expression faciale pour s'assurer que vous ne semblez pas trop sérieux ou inaccessible.",
176
+ "<strong>Qualité d'Éclairage :</strong> Le système évalue la confiance de détection. Un score faible signifie que la photo a du bruit visuel ou des ombres dures que l'algorithme Tinder pourrait signaler comme contenu de mauvaise qualité.",
177
+ ],
178
+ },
179
+ {
180
+ type: 'title',
181
+ text: "Métadonnées et Export Optimisé pour Tinder",
182
+ level: 3,
183
+ },
184
+ {
185
+ type: 'paragraph',
186
+ html: "Saviez-vous que le poids de votre image influence l'algorithme ? Les applications de rencontres privilégient le contenu qui se charge rapidement pour ne pas interrompre le flux de l'utilisateur.",
187
+ },
188
+ {
189
+ type: 'list',
190
+ items: [
191
+ "<strong>Suppression des Métadonnées (Confidentialité et SEO) :</strong> Nous supprimons les enregistrements EXIF. Une photo propre donne à l'algorithme de la plateforme une ardoise vierge.",
192
+ "<strong>Compression Intelligente sans Perte :</strong> Nous réduisons le poids du fichier JPG tout en maintenant l'espace colorimétrique sRGB natif d'iOS et Android. Votre photo sera vibrante même avec peu de signal.",
193
+ "<strong>Format 9:16 Immersif :</strong> En livrant exactement 1080x1920 pixels, nous évitons que Tinder n'applique son propre algorithme de compression agressif qui génère des artefacts d'image.",
194
+ ],
195
+ },
196
+ {
197
+ type: 'tip',
198
+ title: "Conseil d'Expert",
199
+ html: "<p>La première photo doit TOUJOURS être un portrait clair des épaules vers le haut. Gardez les photos plein corps ou d'activités pour la 3ème ou 4ème position. Assurez-vous que votre regard est dégagé et que votre visage a la taille optimale (30-50%).</p>",
200
+ },
201
+ {
202
+ type: 'title',
203
+ text: "Sécurité et Éthique du Traitement",
204
+ level: 3,
205
+ },
206
+ {
207
+ type: 'paragraph',
208
+ html: "Nous comprenons que vos photos sont privées. Cet optimiseur utilise la <strong>technologie Client-Side</strong> : les images sont traitées via un moteur canvas dans votre navigateur local. Aucune donnée ne quitte votre appareil. C'est la façon la plus sûre et la plus rapide d'optimiser votre profil sans compromettre votre vie privée.",
209
+ },
210
+ ],
211
+ ui: {
212
+ uploadTitle: 'Optimiser la Photo',
213
+ uploadSubtitle: 'Faites glisser ou cliquez pour commencer',
214
+ btnDownload: 'Télécharger la Photo Optimisée',
215
+ btnRescan: 'Rescanner le Visage',
216
+ btnAutoFocus: 'Cadrage Auto',
217
+ btnMarkFace: 'Marquer le Visage',
218
+ panelManual: 'Ajustements Manuels',
219
+ panelVisualization: 'Visualisation Avancée',
220
+ panelMetrics: "Analytique de Composition",
221
+ panelExport: 'Exporter pour Tinder 2026',
222
+ panelExportDesc: 'Recadrage automatique en 1080x1920 avec métadonnées optimisées.',
223
+ labelZoom: 'Zoom',
224
+ labelBrightness: 'Luminosité',
225
+ toggleGrid: 'Grille des Tiers',
226
+ toggleGridDesc: 'Ajustement de composition classique',
227
+ toggleDeadzones: 'Zones Dangereuses',
228
+ toggleDeadzonesDesc: "Évite que l'UI cache votre visage",
229
+ toggleFaceBox: 'Voir le Visage Détecté',
230
+ toggleFaceBoxDesc: 'Landmarks et zone de détection',
231
+ metricFaceSize: 'Ratio Visage',
232
+ metricEyeLine: 'Ligne du Regard',
233
+ metricCoverage: 'Couverture',
234
+ metricMood: 'Expression',
235
+ metricQuality: 'Confiance',
236
+ analysisSuccess: 'Facteurs de Succès',
237
+ analysisWarning: 'Optimisation',
238
+ analysisError: 'Erreurs Critiques',
239
+ deadzoneTop: "Zone UI Supérieure",
240
+ deadzoneBottom: "Zone d'Information",
241
+ loaderAnalyzing: 'Analyse de la composition...',
242
+ loaderModels: 'Chargement des modèles de vision...',
243
+ simulatorName: 'Votre Nom, 25',
244
+ simulatorJob: 'Votre Profession',
245
+ simulatorCity: 'Vit à Paris',
246
+ simulatorDistance: 'À 5 km de vous',
247
+ },
248
+ };
@@ -0,0 +1,34 @@
1
+ import type { SocialToolEntry, ToolLocaleContent, ToolDefinition } from '../../types';
2
+ import TinderPhotoOptimizerComponent from './component.astro';
3
+ import TinderPhotoOptimizerSEO from './seo.astro';
4
+ import TinderPhotoOptimizerBibliography from './bibliography.astro';
5
+
6
+ import type { TinderPhotoOptimizerUI } from './ui';
7
+
8
+ export type TinderPhotoOptimizerLocaleContent = ToolLocaleContent<TinderPhotoOptimizerUI>;
9
+
10
+ import { content as es } from './i18n/es';
11
+ import { content as en } from './i18n/en';
12
+ import { content as fr } from './i18n/fr';
13
+
14
+ export const tinderPhotoOptimizer: SocialToolEntry<TinderPhotoOptimizerUI> = {
15
+ id: 'tinder-photo-optimizer',
16
+ icons: {
17
+ bg: 'mdi:fire',
18
+ fg: 'mdi:image-edit-outline',
19
+ },
20
+ i18n: {
21
+ es: async () => es,
22
+ en: async () => en,
23
+ fr: async () => fr,
24
+ },
25
+ };
26
+
27
+ export { TinderPhotoOptimizerComponent, TinderPhotoOptimizerSEO, TinderPhotoOptimizerBibliography };
28
+
29
+ export const TINDER_PHOTO_OPTIMIZER_TOOL: ToolDefinition = {
30
+ entry: tinderPhotoOptimizer,
31
+ Component: TinderPhotoOptimizerComponent,
32
+ SEOComponent: TinderPhotoOptimizerSEO,
33
+ BibliographyComponent: TinderPhotoOptimizerBibliography,
34
+ };
@@ -0,0 +1,199 @@
1
+ interface FaceLandmark { x: number; y: number; }
2
+ interface FaceLandmarks { getLeftEye(): FaceLandmark[]; getRightEye(): FaceLandmark[]; positions: FaceLandmark[]; }
3
+ interface FaceExpressions { happy: number; angry: number; }
4
+ interface FaceBox { x: number; y: number; width: number; height: number; }
5
+ interface FaceNet { loadFromUri(url: string): Promise<void>; }
6
+ interface FaceApiGlobal {
7
+ nets: { tinyFaceDetector: FaceNet; faceLandmark68Net: FaceNet; faceExpressionNet: FaceNet };
8
+ TinyFaceDetectorOptions: new () => unknown;
9
+ detectSingleFace(img: HTMLImageElement, opts: unknown): { withFaceLandmarks(): { withFaceExpressions(): Promise<FaceDetectionResult | undefined> } };
10
+ }
11
+ declare const faceapi: FaceApiGlobal;
12
+
13
+ export interface FaceDetectionResult { detection: { box: FaceBox; score: number }; landmarks: FaceLandmarks | null; expressions: FaceExpressions | null; }
14
+ export interface AnalysisAlert { type: 'success' | 'warning' | 'error'; text: string; }
15
+ type AnalysisCtx = { alerts: AnalysisAlert[]; metrics: Record<string, string> };
16
+ type ImgGeo = { x: number; y: number; w: number; h: number; canvasW: number; canvasH: number; r: number };
17
+ type FaceGeo = { fx: number; fy: number; fw: number; fh: number; faceHeightRatio: number };
18
+
19
+ export class TinderOptimizerEngine {
20
+ private canvas: HTMLCanvasElement;
21
+ private ctx: CanvasRenderingContext2D;
22
+ public image: HTMLImageElement | null = null;
23
+ public faceDetection: FaceDetectionResult | null = null;
24
+ public zoom = 1;
25
+ public offset = { x: 0, y: 0 };
26
+ public brightness = 100;
27
+ private isFaceApiLoaded = false;
28
+
29
+ constructor(canvas: HTMLCanvasElement) {
30
+ this.canvas = canvas;
31
+ this.ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
32
+ }
33
+
34
+ async loadModels() {
35
+ if (this.isFaceApiLoaded) return;
36
+ const MODEL_URL = 'https://justadudewhohacks.github.io/face-api.js/models';
37
+ await Promise.all([
38
+ faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
39
+ faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL),
40
+ faceapi.nets.faceExpressionNet.loadFromUri(MODEL_URL),
41
+ ]);
42
+ this.isFaceApiLoaded = true;
43
+ }
44
+
45
+ async detect(image: HTMLImageElement) {
46
+ this.image = image;
47
+ const detections = await faceapi
48
+ .detectSingleFace(image, new faceapi.TinyFaceDetectorOptions())
49
+ .withFaceLandmarks()
50
+ .withFaceExpressions();
51
+ this.faceDetection = detections ?? null;
52
+ return detections;
53
+ }
54
+
55
+ setManualFace(rect: { x: number; y: number; w: number; h: number }) {
56
+ if (!this.image) return;
57
+ const r = Math.max(this.canvas.width / this.image.width, this.canvas.height / this.image.height);
58
+ const w = this.image.width * r * this.zoom;
59
+ const h = this.image.height * r * this.zoom;
60
+ const imgX = (this.canvas.width - w) / 2 + this.offset.x;
61
+ const imgY = (this.canvas.height - h) / 2 + this.offset.y;
62
+ this.faceDetection = {
63
+ detection: { box: { x: (rect.x - imgX) / (r * this.zoom), y: (rect.y - imgY) / (r * this.zoom), width: rect.w / (r * this.zoom), height: rect.h / (r * this.zoom) }, score: 1 },
64
+ landmarks: null, expressions: null,
65
+ };
66
+ }
67
+
68
+ updateState(zoom: number, offset: { x: number; y: number }, brightness: number) {
69
+ this.zoom = zoom; this.offset = offset; this.brightness = brightness;
70
+ }
71
+
72
+ private computeImgGeo(): ImgGeo {
73
+ const iw = this.image!.width, ih = this.image!.height;
74
+ const canvasW = this.canvas.width, canvasH = this.canvas.height;
75
+ const r = Math.max(canvasW / iw, canvasH / ih);
76
+ const w = iw * r * this.zoom, h = ih * r * this.zoom;
77
+ return { x: (canvasW - w) / 2 + this.offset.x, y: (canvasH - h) / 2 + this.offset.y, w, h, canvasW, canvasH, r };
78
+ }
79
+
80
+ private checkCoverage(ctx: AnalysisCtx, geo: ImgGeo) {
81
+ const { x, y, w, h, canvasW, canvasH } = geo;
82
+ const ok = !(x > 0 || y > 0 || x + w < canvasW || y + h < canvasH);
83
+ ctx.metrics.coverage = ok ? '100%' : 'INCORRECTA';
84
+ if (!ok) ctx.alerts.push({ type: 'error', text: 'ESPACIOS VACÍOS: La imagen no cubre todo el encuadre. Ajusta el zoom o la posición.' });
85
+ }
86
+
87
+ private getFaceGeo(box: FaceBox, geo: ImgGeo): FaceGeo {
88
+ const { x, y, r } = geo;
89
+ const fx = x + box.x * r * this.zoom, fy = y + box.y * r * this.zoom;
90
+ const fw = box.width * r * this.zoom, fh = box.height * r * this.zoom;
91
+ return { fx, fy, fw, fh, faceHeightRatio: fh / geo.canvasH };
92
+ }
93
+
94
+ private analyzeFaceSize(ctx: AnalysisCtx, ratio: number) {
95
+ ctx.metrics.faceSize = `${(ratio * 100).toFixed(1)}%`;
96
+ if (ratio < 0.2) ctx.alerts.push({ type: 'error', text: 'CASI INVISIBLE: Estás muy lejos. Haz zoom hasta que tu cara ocupe al menos un tercio de la pantalla.' });
97
+ else if (ratio > 0.6) ctx.alerts.push({ type: 'warning', text: 'DEMASIADO CERCA: Los primeros planos extremos pueden resultar agresivos.' });
98
+ else if (ratio >= 0.3 && ratio <= 0.5) ctx.alerts.push({ type: 'success', text: 'TAMAÑO PERFECTO: Tu rostro tiene el ratio ideal para transmitir confianza.' });
99
+ }
100
+
101
+ private analyzeEyeLine(ctx: AnalysisCtx, landmarks: FaceLandmarks, geo: ImgGeo) {
102
+ const avgY = (landmarks.getLeftEye()[0].y + landmarks.getRightEye()[0].y) / 2;
103
+ const eyeDiff = (geo.y + avgY * geo.r * this.zoom) - 640;
104
+ if (Math.abs(eyeDiff) < 150) {
105
+ ctx.metrics.eyeLine = 'ÓPTIMA';
106
+ ctx.alerts.push({ type: 'success', text: 'MIRADA IMPACTANTE: Tus ojos están en la línea de atención máxima.' });
107
+ } else {
108
+ ctx.metrics.eyeLine = eyeDiff > 0 ? 'BAJA' : 'ALTA';
109
+ ctx.alerts.push({ type: 'warning', text: `LÍNEA DE OJOS: Para un encuadre profesional, ${eyeDiff > 0 ? 'sube' : 'baja'} un poco el rostro hasta la línea superior.` });
110
+ }
111
+ if (Math.abs(this.estimateRoll(landmarks)) > 15) ctx.alerts.push({ type: 'warning', text: 'CABEZA INCLINADA: Una postura demasiado torcida puede transmitir inestabilidad.' });
112
+ }
113
+
114
+ private analyzeMood(ctx: AnalysisCtx, expr: FaceExpressions) {
115
+ const happy = expr.happy || 0;
116
+ ctx.metrics.mood = happy > 0.7 ? 'POSITIVO' : 'NEUTRAL';
117
+ if (happy > 0.7) ctx.alerts.push({ type: 'success', text: 'SONRISA DETECTADA: Las sonrisas aumentan la tasa de match significativamente.' });
118
+ else if (expr.angry > 0.4) ctx.alerts.push({ type: 'warning', text: 'EXPRESIÓN SEVERA: Intenta usar una foto con un gesto más amable.' });
119
+ }
120
+
121
+ private checkFaceObstruction(ctx: AnalysisCtx, fy: number, fh: number) {
122
+ if (fy < 300) ctx.alerts.push({ type: 'error', text: 'OBSTRUIDO: La barra de progreso tapa tu rostro.' });
123
+ else if (fy + fh > 1400) ctx.alerts.push({ type: 'error', text: 'OBSTRUIDO: El bloque de información tapa tu cara.' });
124
+ }
125
+
126
+ private checkFaceOut(ctx: AnalysisCtx, fg: FaceGeo, canvasW: number, canvasH: number) {
127
+ const { fx, fy, fw, fh } = fg;
128
+ const confidence = this.faceDetection!.detection.score;
129
+ ctx.metrics.quality = confidence > 0.8 ? 'ALTA' : 'MEDIA';
130
+ if (confidence < 0.6) ctx.alerts.push({ type: 'warning', text: 'ILUMINACIÓN DEFICIENTE: La IA tiene dificultades para verte. Busca una foto con mejor luz.' });
131
+ if (fx < 0 || fx + fw > canvasW || fy < 0 || fy + fh > canvasH) ctx.alerts.push({ type: 'error', text: 'CORTADO: El rostro se sale del encuadre vertical.' });
132
+ this.checkFaceObstruction(ctx, fy, fh);
133
+ }
134
+
135
+ getAnalysis() {
136
+ const alerts: AnalysisAlert[] = [];
137
+ const metrics: Record<string, string> = { faceSize: '0%', eyeLine: '--', coverage: '0%', mood: '--', quality: '--' };
138
+ if (!this.image) return { alerts, metrics };
139
+ const ctx: AnalysisCtx = { alerts, metrics };
140
+ const geo = this.computeImgGeo();
141
+ this.checkCoverage(ctx, geo);
142
+ if (this.faceDetection) {
143
+ const fg = this.getFaceGeo(this.faceDetection.detection.box, geo);
144
+ this.analyzeFaceSize(ctx, fg.faceHeightRatio);
145
+ if (this.faceDetection.landmarks) this.analyzeEyeLine(ctx, this.faceDetection.landmarks, geo);
146
+ if (this.faceDetection.expressions) this.analyzeMood(ctx, this.faceDetection.expressions);
147
+ this.checkFaceOut(ctx, fg, geo.canvasW, geo.canvasH);
148
+ }
149
+ if (this.zoom < 1.15) alerts.push({ type: 'warning', text: 'CONSEJO PRO: Si es una selfie, usa el Zoom (1.2x) para evitar distorsiones de lente.' });
150
+ return { alerts, metrics };
151
+ }
152
+
153
+ private estimateRoll(landmarks: FaceLandmarks): number {
154
+ const l = landmarks.getLeftEye()[0], r = landmarks.getRightEye()[0];
155
+ return Math.atan2(r.y - l.y, r.x - l.x) * (180 / Math.PI);
156
+ }
157
+
158
+ getOptimalTransform() {
159
+ if (!this.image || !this.faceDetection) return null;
160
+ const box = this.faceDetection.detection.box;
161
+ const r = Math.max(this.canvas.width / this.image.width, this.canvas.height / this.image.height);
162
+ const optimalZoom = Math.min(Math.max((this.canvas.height * 0.25) / (box.height * r), 1), 3);
163
+ const faceCX = box.x + box.width / 2, faceCY = box.y + box.height / 2;
164
+ const w = this.image.width * r * optimalZoom, h = this.image.height * r * optimalZoom;
165
+ return { zoom: optimalZoom, x: 540 - (this.canvas.width - w) / 2 - faceCX * r * optimalZoom, y: 800 - (this.canvas.height - h) / 2 - faceCY * r * optimalZoom };
166
+ }
167
+
168
+ private drawFaceBox(x: number, y: number, r: number) {
169
+ const box = this.faceDetection!.detection.box;
170
+ const fx = x + box.x * r * this.zoom, fy = y + box.y * r * this.zoom;
171
+ const fw = box.width * r * this.zoom, fh = box.height * r * this.zoom;
172
+ this.ctx.strokeStyle = '#00ff00'; this.ctx.lineWidth = 6;
173
+ this.ctx.strokeRect(fx, fy, fw, fh);
174
+ this.ctx.fillStyle = '#00ff00'; this.ctx.font = 'bold 40px Arial';
175
+ this.ctx.fillText('Cara detectada', fx, fy - 10);
176
+ const landmarks = this.faceDetection!.landmarks;
177
+ if (landmarks) {
178
+ this.ctx.fillStyle = 'rgba(0, 255, 0, 0.5)';
179
+ landmarks.positions.forEach((p: FaceLandmark) => {
180
+ this.ctx.beginPath();
181
+ this.ctx.arc(x + p.x * r * this.zoom, y + p.y * r * this.zoom, 2, 0, 2 * Math.PI);
182
+ this.ctx.fill();
183
+ });
184
+ }
185
+ }
186
+
187
+ draw(showFaceBox: boolean) {
188
+ if (!this.image) return;
189
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
190
+ this.ctx.filter = `brightness(${this.brightness}%)`;
191
+ const iw = this.image.width, ih = this.image.height;
192
+ const r = Math.max(this.canvas.width / iw, this.canvas.height / ih);
193
+ const w = iw * r * this.zoom, h = ih * r * this.zoom;
194
+ const x = (this.canvas.width - w) / 2 + this.offset.x;
195
+ const y = (this.canvas.height - h) / 2 + this.offset.y;
196
+ this.ctx.drawImage(this.image, x, y, w, h);
197
+ if (showFaceBox && this.faceDetection) this.drawFaceBox(x, y, r);
198
+ }
199
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { tinderPhotoOptimizer } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await tinderPhotoOptimizer.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+ <SEORenderer content={{ locale: locale as string, sections: content.seo }} />
@@ -0,0 +1,37 @@
1
+ export interface TinderPhotoOptimizerUI extends Record<string, string> {
2
+ uploadTitle: string;
3
+ uploadSubtitle: string;
4
+ btnDownload: string;
5
+ btnRescan: string;
6
+ btnAutoFocus: string;
7
+ btnMarkFace: string;
8
+ panelManual: string;
9
+ panelVisualization: string;
10
+ panelMetrics: string;
11
+ panelExport: string;
12
+ panelExportDesc: string;
13
+ labelZoom: string;
14
+ labelBrightness: string;
15
+ toggleGrid: string;
16
+ toggleGridDesc: string;
17
+ toggleDeadzones: string;
18
+ toggleDeadzonesDesc: string;
19
+ toggleFaceBox: string;
20
+ toggleFaceBoxDesc: string;
21
+ metricFaceSize: string;
22
+ metricEyeLine: string;
23
+ metricCoverage: string;
24
+ metricMood: string;
25
+ metricQuality: string;
26
+ analysisSuccess: string;
27
+ analysisWarning: string;
28
+ analysisError: string;
29
+ deadzoneTop: string;
30
+ deadzoneBottom: string;
31
+ loaderAnalyzing: string;
32
+ loaderModels: string;
33
+ simulatorName: string;
34
+ simulatorJob: string;
35
+ simulatorCity: string;
36
+ simulatorDistance: string;
37
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { youtubeThumbnail } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await youtubeThumbnail.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}