@jjlmoya/utils-audiovisual 1.2.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.
- package/package.json +60 -0
- package/src/category/i18n/en.ts +198 -0
- package/src/category/i18n/es.ts +198 -0
- package/src/category/i18n/fr.ts +198 -0
- package/src/category/index.ts +17 -0
- package/src/category/seo.astro +15 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +4 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +32 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +146 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +19 -0
- package/src/tests/locale_completeness.test.ts +42 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/seo_length.test.ts +22 -0
- package/src/tests/tool_validation.test.ts +17 -0
- package/src/tool/chromaticLens/bibliography.astro +17 -0
- package/src/tool/chromaticLens/component.astro +178 -0
- package/src/tool/chromaticLens/i18n/en.ts +246 -0
- package/src/tool/chromaticLens/i18n/es.ts +244 -0
- package/src/tool/chromaticLens/i18n/fr.ts +244 -0
- package/src/tool/chromaticLens/index.ts +43 -0
- package/src/tool/chromaticLens/logic.ts +87 -0
- package/src/tool/chromaticLens/seo.astro +15 -0
- package/src/tool/chromaticLens/style.css +308 -0
- package/src/tool/chromaticLens/ui.ts +109 -0
- package/src/tool/collageMaker/bibliography.astro +17 -0
- package/src/tool/collageMaker/component.astro +302 -0
- package/src/tool/collageMaker/i18n/en.ts +233 -0
- package/src/tool/collageMaker/i18n/es.ts +231 -0
- package/src/tool/collageMaker/i18n/fr.ts +231 -0
- package/src/tool/collageMaker/index.ts +51 -0
- package/src/tool/collageMaker/logic.ts +134 -0
- package/src/tool/collageMaker/seo.astro +15 -0
- package/src/tool/collageMaker/style.css +386 -0
- package/src/tool/exifCleaner/bibliography.astro +18 -0
- package/src/tool/exifCleaner/component.astro +162 -0
- package/src/tool/exifCleaner/i18n/en.ts +277 -0
- package/src/tool/exifCleaner/i18n/es.ts +277 -0
- package/src/tool/exifCleaner/i18n/fr.ts +277 -0
- package/src/tool/exifCleaner/index.ts +57 -0
- package/src/tool/exifCleaner/logic.ts +135 -0
- package/src/tool/exifCleaner/seo.astro +18 -0
- package/src/tool/exifCleaner/style.css +289 -0
- package/src/tool/exifCleaner/ui.ts +117 -0
- package/src/tool/imageCompressor/bibliography.astro +17 -0
- package/src/tool/imageCompressor/component.astro +262 -0
- package/src/tool/imageCompressor/i18n/en.ts +232 -0
- package/src/tool/imageCompressor/i18n/es.ts +230 -0
- package/src/tool/imageCompressor/i18n/fr.ts +230 -0
- package/src/tool/imageCompressor/index.ts +50 -0
- package/src/tool/imageCompressor/logic.ts +79 -0
- package/src/tool/imageCompressor/seo.astro +15 -0
- package/src/tool/imageCompressor/style.css +503 -0
- package/src/tool/printQualityCalculator/bibliography.astro +18 -0
- package/src/tool/printQualityCalculator/component.astro +318 -0
- package/src/tool/printQualityCalculator/i18n/en.ts +247 -0
- package/src/tool/printQualityCalculator/i18n/es.ts +245 -0
- package/src/tool/printQualityCalculator/i18n/fr.ts +245 -0
- package/src/tool/printQualityCalculator/index.ts +56 -0
- package/src/tool/printQualityCalculator/logic.ts +53 -0
- package/src/tool/printQualityCalculator/seo.astro +18 -0
- package/src/tool/printQualityCalculator/style.css +491 -0
- package/src/tool/printQualityCalculator/ui.ts +122 -0
- package/src/tool/privacyBlur/bibliography.astro +17 -0
- package/src/tool/privacyBlur/component.astro +230 -0
- package/src/tool/privacyBlur/i18n/en.ts +238 -0
- package/src/tool/privacyBlur/i18n/es.ts +236 -0
- package/src/tool/privacyBlur/i18n/fr.ts +236 -0
- package/src/tool/privacyBlur/index.ts +49 -0
- package/src/tool/privacyBlur/logic.ts +249 -0
- package/src/tool/privacyBlur/seo.astro +15 -0
- package/src/tool/privacyBlur/style.css +332 -0
- package/src/tool/privacyBlur/ui.ts +124 -0
- package/src/tool/subtitleSync/bibliography.astro +17 -0
- package/src/tool/subtitleSync/component.astro +187 -0
- package/src/tool/subtitleSync/i18n/en.ts +241 -0
- package/src/tool/subtitleSync/i18n/es.ts +241 -0
- package/src/tool/subtitleSync/i18n/fr.ts +241 -0
- package/src/tool/subtitleSync/index.ts +49 -0
- package/src/tool/subtitleSync/logic.ts +91 -0
- package/src/tool/subtitleSync/seo.astro +15 -0
- package/src/tool/subtitleSync/style.css +325 -0
- package/src/tool/subtitleSync/ui.ts +152 -0
- package/src/tool/timelapseCalculator/bibliography.astro +15 -0
- package/src/tool/timelapseCalculator/component.astro +148 -0
- package/src/tool/timelapseCalculator/i18n/en.ts +169 -0
- package/src/tool/timelapseCalculator/i18n/es.ts +169 -0
- package/src/tool/timelapseCalculator/i18n/fr.ts +169 -0
- package/src/tool/timelapseCalculator/index.ts +52 -0
- package/src/tool/timelapseCalculator/logic.ts +46 -0
- package/src/tool/timelapseCalculator/seo.astro +18 -0
- package/src/tool/timelapseCalculator/style.css +285 -0
- package/src/tool/tvDistance/bibliography.astro +17 -0
- package/src/tool/tvDistance/component.astro +178 -0
- package/src/tool/tvDistance/i18n/en.ts +223 -0
- package/src/tool/tvDistance/i18n/es.ts +223 -0
- package/src/tool/tvDistance/i18n/fr.ts +223 -0
- package/src/tool/tvDistance/index.ts +49 -0
- package/src/tool/tvDistance/logic.ts +47 -0
- package/src/tool/tvDistance/seo.astro +15 -0
- package/src/tool/tvDistance/style.css +435 -0
- package/src/tool/tvDistance/ui.ts +66 -0
- package/src/tool/videoFrameExtractor/bibliography.astro +17 -0
- package/src/tool/videoFrameExtractor/component.astro +285 -0
- package/src/tool/videoFrameExtractor/i18n/en.ts +235 -0
- package/src/tool/videoFrameExtractor/i18n/es.ts +235 -0
- package/src/tool/videoFrameExtractor/i18n/fr.ts +235 -0
- package/src/tool/videoFrameExtractor/index.ts +53 -0
- package/src/tool/videoFrameExtractor/logic.ts +49 -0
- package/src/tool/videoFrameExtractor/seo.astro +15 -0
- package/src/tool/videoFrameExtractor/style.css +426 -0
- package/src/tool/videoFrameExtractor/ui.ts +179 -0
- package/src/tools.ts +25 -0
- package/src/types.ts +72 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { subtitleSync } 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 subtitleSync.i18n[locale]?.();
|
|
12
|
+
if (!content || !content.bibliography || content.bibliography.length === 0) return null;
|
|
13
|
+
|
|
14
|
+
const { bibliography, bibliographyTitle = 'Bibliografía' } = content;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<SharedBibliography title={bibliographyTitle} links={bibliography} />
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { SubtitleSyncUI } from './index';
|
|
3
|
+
import './style.css';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: SubtitleSyncUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="ss-root" id="ss-root" data-ui={JSON.stringify(ui)}>
|
|
13
|
+
<div class="ss-card">
|
|
14
|
+
|
|
15
|
+
<div id="ss-drop" class="ss-drop">
|
|
16
|
+
<input type="file" id="ss-file" accept=".srt,.vtt" class="ss-hidden" />
|
|
17
|
+
<div class="ss-drop-icon">
|
|
18
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
19
|
+
<path d="M17 6h-1V3H8v3H7c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-5 2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm5 12H7V8h10v12zm-1-8h-2v4h2v-4zm-4 0H8v4h2v-4z"/>
|
|
20
|
+
</svg>
|
|
21
|
+
</div>
|
|
22
|
+
<h3 class="ss-drop-title">{ui.dropTitle}</h3>
|
|
23
|
+
<p class="ss-drop-sub">{ui.dropSubtitle}</p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div id="ss-controls" class="ss-controls ss-hidden">
|
|
27
|
+
<div class="ss-control-group">
|
|
28
|
+
<span class="ss-control-label">{ui.offsetLabel}</span>
|
|
29
|
+
<div class="ss-offset-wrap">
|
|
30
|
+
<button id="ss-dec" class="ss-offset-btn">
|
|
31
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
32
|
+
<path d="M19 13H5v-2h14v2z"/>
|
|
33
|
+
</svg>
|
|
34
|
+
</button>
|
|
35
|
+
<input type="number" id="ss-offset" value="0.0" step="0.1" class="ss-offset-input" />
|
|
36
|
+
<button id="ss-inc" class="ss-offset-btn">
|
|
37
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
38
|
+
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
|
39
|
+
</svg>
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="ss-stats">
|
|
44
|
+
<div class="ss-stat">
|
|
45
|
+
<span class="ss-stat-value" id="ss-count">0</span>
|
|
46
|
+
<span class="ss-stat-label">{ui.linesStat}</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="ss-status-card">
|
|
52
|
+
<div class="ss-file-row">
|
|
53
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="ss-file-icon">
|
|
54
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM16 18H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L19.5 9H13z"/>
|
|
55
|
+
</svg>
|
|
56
|
+
<div class="ss-file-info">
|
|
57
|
+
<span class="ss-file-name" id="ss-name">subtítulos.srt</span>
|
|
58
|
+
<div class="ss-file-times">
|
|
59
|
+
<span id="ss-first">00:00:00</span> — <span id="ss-last">00:00:00</span>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<button id="ss-download" class="ss-btn-primary">
|
|
65
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
66
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM14 20H6v-2h8v2zm0-4H6v-2h8v2zm0-4H6V8h8v4z"/>
|
|
67
|
+
</svg>
|
|
68
|
+
<span>{ui.downloadButton}</span>
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="ss-previews">
|
|
73
|
+
<div class="ss-preview-box">
|
|
74
|
+
<h5 class="ss-preview-label">{ui.originalLabel}</h5>
|
|
75
|
+
<div id="ss-preview-orig" class="ss-preview-scroll"></div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="ss-preview-box">
|
|
78
|
+
<h5 class="ss-preview-label ss-preview-label-modified">{ui.resultLabel}</h5>
|
|
79
|
+
<div id="ss-preview-mod" class="ss-preview-scroll"></div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<script>
|
|
88
|
+
import { parseSRT, shiftSubtitles, generateSRT, msToTime } from './logic';
|
|
89
|
+
import type { SrtItem } from './logic';
|
|
90
|
+
|
|
91
|
+
function escapeHtml(text: string): string {
|
|
92
|
+
const map: { [key: string]: string } = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
93
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function init() {
|
|
97
|
+
const root = document.getElementById('ss-root');
|
|
98
|
+
if (!root) return;
|
|
99
|
+
const dropZone = root.querySelector('#ss-drop') as HTMLElement;
|
|
100
|
+
const fileInput = root.querySelector('#ss-file') as HTMLInputElement;
|
|
101
|
+
const controls = root.querySelector('#ss-controls') as HTMLElement;
|
|
102
|
+
const offsetInput = root.querySelector('#ss-offset') as HTMLInputElement;
|
|
103
|
+
const decBtn = root.querySelector('#ss-dec') as HTMLButtonElement;
|
|
104
|
+
const incBtn = root.querySelector('#ss-inc') as HTMLButtonElement;
|
|
105
|
+
const countEl = root.querySelector('#ss-count') as HTMLElement;
|
|
106
|
+
const fileName = root.querySelector('#ss-name') as HTMLElement;
|
|
107
|
+
const firstTime = root.querySelector('#ss-first') as HTMLElement;
|
|
108
|
+
const lastTime = root.querySelector('#ss-last') as HTMLElement;
|
|
109
|
+
const downloadBtn = root.querySelector('#ss-download') as HTMLButtonElement;
|
|
110
|
+
const previewOrig = root.querySelector('#ss-preview-orig') as HTMLElement;
|
|
111
|
+
const previewMod = root.querySelector('#ss-preview-mod') as HTMLElement;
|
|
112
|
+
|
|
113
|
+
let originalItems: SrtItem[] = [];
|
|
114
|
+
let currentFileName = 'subtitles.srt';
|
|
115
|
+
|
|
116
|
+
function renderPreview(container: HTMLElement, items: SrtItem[]) {
|
|
117
|
+
container.innerHTML = '';
|
|
118
|
+
items.slice(0, 5).forEach((item) => {
|
|
119
|
+
const div = document.createElement('div');
|
|
120
|
+
div.className = 'ss-preview-item';
|
|
121
|
+
div.innerHTML = `
|
|
122
|
+
<p class="ss-preview-time">${msToTime(item.start)} → ${msToTime(item.end)}</p>
|
|
123
|
+
<p class="ss-preview-text">${escapeHtml(item.text)}</p>
|
|
124
|
+
`;
|
|
125
|
+
container.appendChild(div);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function updateUI() {
|
|
130
|
+
const offset = (parseFloat(offsetInput.value) || 0) * 1000;
|
|
131
|
+
const modified = shiftSubtitles(originalItems, offset);
|
|
132
|
+
countEl.textContent = modified.length.toString();
|
|
133
|
+
if (modified.length > 0) {
|
|
134
|
+
firstTime.textContent = msToTime(modified[0].start);
|
|
135
|
+
lastTime.textContent = msToTime(modified[modified.length - 1].end);
|
|
136
|
+
}
|
|
137
|
+
renderPreview(previewOrig, originalItems);
|
|
138
|
+
renderPreview(previewMod, modified);
|
|
139
|
+
downloadBtn.onclick = () => {
|
|
140
|
+
const content = generateSRT(modified);
|
|
141
|
+
const blob = new Blob([content], { type: 'text/srt' });
|
|
142
|
+
const url = URL.createObjectURL(blob);
|
|
143
|
+
const a = document.createElement('a');
|
|
144
|
+
a.href = url;
|
|
145
|
+
const parts = currentFileName.split('.');
|
|
146
|
+
a.download = parts.length > 1 ? `${parts.slice(0, -1).join('.')}_fixed.${parts[parts.length - 1]}` : `${currentFileName}_fixed.srt`;
|
|
147
|
+
a.click();
|
|
148
|
+
URL.revokeObjectURL(url);
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function handleFile(file: File) {
|
|
153
|
+
currentFileName = file.name;
|
|
154
|
+
fileName.textContent = file.name;
|
|
155
|
+
const reader = new FileReader();
|
|
156
|
+
reader.onload = (e) => {
|
|
157
|
+
const text = e.target?.result;
|
|
158
|
+
if (typeof text === 'string') {
|
|
159
|
+
originalItems = parseSRT(text);
|
|
160
|
+
if (originalItems.length > 0) {
|
|
161
|
+
controls.classList.remove('ss-hidden');
|
|
162
|
+
dropZone.classList.add('ss-hidden');
|
|
163
|
+
updateUI();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
reader.readAsText(file);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
dropZone.addEventListener('click', () => fileInput.click());
|
|
171
|
+
fileInput.addEventListener('change', () => { if (fileInput.files?.[0]) handleFile(fileInput.files[0]); });
|
|
172
|
+
decBtn.addEventListener('click', () => { offsetInput.value = (parseFloat(offsetInput.value) - 0.1).toFixed(1); updateUI(); });
|
|
173
|
+
incBtn.addEventListener('click', () => { offsetInput.value = (parseFloat(offsetInput.value) + 0.1).toFixed(1); updateUI(); });
|
|
174
|
+
offsetInput.addEventListener('input', updateUI);
|
|
175
|
+
|
|
176
|
+
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('ss-drop-active'); });
|
|
177
|
+
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('ss-drop-active'));
|
|
178
|
+
dropZone.addEventListener('drop', (e) => {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
dropZone.classList.remove('ss-drop-active');
|
|
181
|
+
if (e.dataTransfer?.files[0]) handleFile(e.dataTransfer.files[0]);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
init();
|
|
186
|
+
document.addEventListener('astro:page-load', init);
|
|
187
|
+
</script>
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
2
|
+
import type { SubtitleSyncUI, SubtitleSyncLocaleContent } from '../index';
|
|
3
|
+
|
|
4
|
+
const slug = 'synchronize-srt-subtitles-online-adjust-timing-free';
|
|
5
|
+
const title = 'Synchronize SRT Subtitles Online - Adjust Timing for Free';
|
|
6
|
+
const description = 'Online tool to advance or delay SRT subtitles. Correct timing offset easily and download the synchronized file instantly.';
|
|
7
|
+
|
|
8
|
+
const ui: SubtitleSyncUI = {
|
|
9
|
+
dropTitle: "Drag your .SRT file here",
|
|
10
|
+
dropSubtitle: "or click to browse",
|
|
11
|
+
adjustTitle: "Adjust Time",
|
|
12
|
+
offsetLabel: "Offset (seconds)",
|
|
13
|
+
offsetHelp: "Use negative values to advance (e.g., -1.5) and positive values to delay.",
|
|
14
|
+
linesStat: "Lines",
|
|
15
|
+
firstStat: "First Subtitle",
|
|
16
|
+
lastStat: "Last Subtitle",
|
|
17
|
+
originalLabel: "ORIGINAL",
|
|
18
|
+
resultLabel: "RESULT",
|
|
19
|
+
downloadButton: "Download Corrected",
|
|
20
|
+
previewBadge: "PREVIEW",
|
|
21
|
+
unitSeconds: "sec"
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const faq: SubtitleSyncLocaleContent['faq'] = [
|
|
25
|
+
{
|
|
26
|
+
question: "How can I synchronize my subtitles if the audio is ahead?",
|
|
27
|
+
answer: "If the audio appears before the text, you must delay the subtitles. Enter a positive value in the calculator (e.g., 2.0 to delay them by 2 seconds).",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
question: "What file formats does this tool accept?",
|
|
31
|
+
answer: "Currently, the tool is optimized for .SRT (SubRip) files, which is the most common standard in video players and streaming platforms.",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
question: "Is it safe to upload my subtitle files?",
|
|
35
|
+
answer: "Yes, because processing is 100% local on your device. Your files are not sent to any server; synchronization happens directly in your browser.",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
question: "Can I synchronize only part of the file?",
|
|
39
|
+
answer: "No, this tool applies a constant offset to the entire file. If the offset is progressive, you might need more advanced editing.",
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const howTo: SubtitleSyncLocaleContent['howTo'] = [
|
|
44
|
+
{
|
|
45
|
+
name: "Upload your SRT file",
|
|
46
|
+
text: "Drag the subtitle file you want to correct into the upload area.",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "Identify the offset",
|
|
50
|
+
text: "Measure how much delay or advance the subtitles have relative to the audio in your player.",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "Adjust the offset",
|
|
54
|
+
text: "Enter positive (delay) or negative (advance) seconds in the control panel.",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "Download the file",
|
|
58
|
+
text: "Verify in the preview that the times are correct and click download to get your new SRT.",
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const bibliography: SubtitleSyncLocaleContent['bibliography'] = [
|
|
63
|
+
{
|
|
64
|
+
name: "SubRip (SRT) format specification",
|
|
65
|
+
url: "https://matroska.org/technical/subtitles.html#srt-subtitles",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "MDN Web Docs - FileReader API",
|
|
69
|
+
url: "https://developer.mozilla.org/en-US/docs/Web/API/FileReader",
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const seo: SubtitleSyncLocaleContent['seo'] = [
|
|
74
|
+
{
|
|
75
|
+
type: 'summary',
|
|
76
|
+
title: 'Professional Subtitle Synchronization',
|
|
77
|
+
items: [
|
|
78
|
+
'Instant correction of audio-subtitle offsets',
|
|
79
|
+
'Supports standard SRT (SubRip) files',
|
|
80
|
+
'100% local processing - maximum privacy',
|
|
81
|
+
'No installation, no subscription, completely free'
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{ type: 'title', text: 'Perfect SRT Subtitle Synchronization', level: 2 },
|
|
85
|
+
{ type: 'paragraph', html: 'There is nothing more frustrating for a viewer than seeing dialogues that do not match the voices. Subtitle offset usually occurs due to differences between video versions: variations in framerate, advertising cuts, production intros, or changes in compression. With this tool, you fix the problem in seconds.' },
|
|
86
|
+
|
|
87
|
+
{ type: 'stats', items: [
|
|
88
|
+
{ value: '100%', label: 'Local Processing', icon: 'mdi:shield' },
|
|
89
|
+
{ value: 'Milliseconds', label: 'Precision', icon: 'mdi:clock-outline' },
|
|
90
|
+
{ value: 'Any Size', label: 'NO File Limit', icon: 'mdi:file-document' }
|
|
91
|
+
], columns: 3 },
|
|
92
|
+
|
|
93
|
+
{ type: 'title', text: 'Advance vs Delay: Practical Guide', level: 3 },
|
|
94
|
+
{ type: 'paragraph', html: 'The first step is to correctly identify the type of offset. Here is the logic:' },
|
|
95
|
+
{ type: 'list', items: [
|
|
96
|
+
'<strong>Delay (Positive Value):</strong> When you see text appear BEFORE the sound. Subtitles are ahead. Example: +2.0 seconds.',
|
|
97
|
+
'<strong>Advance (Negative Value):</strong> When you see text appear AFTER the sound. Subtitles are behind. Example: -2.0 seconds.',
|
|
98
|
+
'<strong>Test and Adjust:</strong> Start with small increments (0.5s) and use the preview to validate.'
|
|
99
|
+
], icon: 'mdi:arrow-right' },
|
|
100
|
+
|
|
101
|
+
{ type: 'card', title: 'Professional Level Privacy', html: 'By processing the file via client-side JavaScript, we guarantee that your subtitle content never leaves your computer. Essential for translators and professionals handling confidential material or under NDA.' },
|
|
102
|
+
|
|
103
|
+
{ type: 'title', text: 'Common Use Cases', level: 3 },
|
|
104
|
+
{ type: 'comparative', items: [
|
|
105
|
+
{
|
|
106
|
+
title: 'Translators and Subtitlers',
|
|
107
|
+
description: 'Synchronize translations after working with multiple video versions',
|
|
108
|
+
icon: 'mdi:translate',
|
|
109
|
+
points: [
|
|
110
|
+
'SRT files from different sources',
|
|
111
|
+
'Content versioning (theatrical vs streaming)',
|
|
112
|
+
'Fast delivery without changing tools'
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
title: 'Content Creators',
|
|
117
|
+
description: 'Recover subtitles when video was processed with different framerate',
|
|
118
|
+
icon: 'mdi:video',
|
|
119
|
+
points: [
|
|
120
|
+
'Reuse existing subtitles',
|
|
121
|
+
'Format changes (720p to 1080p)',
|
|
122
|
+
'Avoid manual retiming of 1000+ lines'
|
|
123
|
+
],
|
|
124
|
+
highlight: true
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
title: 'Casual Users',
|
|
128
|
+
description: 'Correct downloaded subtitles that don\'t fit perfectly',
|
|
129
|
+
icon: 'mdi:account',
|
|
130
|
+
points: [
|
|
131
|
+
'Generic out-of-sync subtitles',
|
|
132
|
+
'Movies in different region (PAL vs NTSC)',
|
|
133
|
+
'Streaming with edited versions'
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
], columns: 3 },
|
|
137
|
+
|
|
138
|
+
{ type: 'title', text: 'Why Subtitles Go Out of Sync', level: 3 },
|
|
139
|
+
{ type: 'table', headers: ['Common Cause', 'Technical Description', 'Solution'], rows: [
|
|
140
|
+
['Framerate Difference', '23.976 fps vs 25 fps - accumulated difference', 'Single offset adjustment (this tool)'],
|
|
141
|
+
['Editorialization', 'Ad cuts or additional content removed', 'Manual calculation + synchronization'],
|
|
142
|
+
['Regional Version', 'PAL (25 fps Europe) vs NTSC (29.97 fps USA)', 'Simple mathematical offset'],
|
|
143
|
+
['Resolution Change', 'Re-encoding with different processing speed', 'Recalculation of original file']
|
|
144
|
+
] },
|
|
145
|
+
|
|
146
|
+
{ type: 'diagnostic', variant: 'info', title: 'Technical Limitations to Consider', icon: 'mdi:information', badge: 'Important', html: 'This tool applies a <strong>constant</strong> offset to the entire file. If the offset is <strong>progressive</strong> (starts well but gradually goes out of sync), it indicates a persistent framerate difference. In that case, the original file needs re-processing in professional editing software.' },
|
|
147
|
+
|
|
148
|
+
{ type: 'proscons', items: [
|
|
149
|
+
{
|
|
150
|
+
pro: 'Extreme speed - processes large files in milliseconds',
|
|
151
|
+
con: 'Only adjusts fixed offset, not progressive ones'
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
pro: 'Total privacy - content never leaves your browser',
|
|
155
|
+
con: 'Requires modern browser with JavaScript enabled'
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
pro: 'Universal format - works with any standard SRT',
|
|
159
|
+
con: 'Does not support other formats (ASS, VTT, SCC, etc.)'
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
pro: 'Completely free, no advertising, no tracking',
|
|
163
|
+
con: 'No change history or versioning'
|
|
164
|
+
}
|
|
165
|
+
], proTitle: 'Advantages', conTitle: 'Limitations' },
|
|
166
|
+
|
|
167
|
+
{ type: 'glossary', items: [
|
|
168
|
+
{
|
|
169
|
+
term: 'SRT (SubRip)',
|
|
170
|
+
definition: 'Most universal subtitle format: text file with sequence numbers, times (hh:mm:ss,mmm), and text. De facto standard in players and platforms.'
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
term: 'Offset',
|
|
174
|
+
definition: 'Fixed amount of time added or subtracted from all times in the file. Can be positive seconds (delay) or negative (advance).'
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
term: 'Framerate (fps)',
|
|
178
|
+
definition: 'Frames per second. 24p (cinema), 25p (PAL/Europe), 29.97p (NTSC/USA), 60p (fluid video). Differences cause cumulative offsets.'
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
term: 'NTSC vs PAL',
|
|
182
|
+
definition: 'Regional television standards: PAL (25 fps) in Europe; NTSC (29.97 fps) in USA. ~4% speed difference.'
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
term: 'Progressive Offset',
|
|
186
|
+
definition: 'When synchronization starts correct but ends up gradually out of sync. Indicates persistent framerate difference, requires re-processing.'
|
|
187
|
+
}
|
|
188
|
+
] },
|
|
189
|
+
|
|
190
|
+
{ type: 'message', title: 'Professional Editing with Full Control', ariaLabel: 'Technical information about synchronization', html: 'Our approach is simple but powerful: a single offset, applied instantly, processed 100% in your browser. No cloud, no storage, no tracking. Simply upload, adjust, download. Full control over your content.' },
|
|
191
|
+
|
|
192
|
+
{ type: 'title', text: 'Conclusion: Movies Without Interruptions', level: 3 },
|
|
193
|
+
{ type: 'paragraph', html: 'Perfect subtitle synchronization is essential for a quality audiovisual experience. With this tool, you transform a frustrating experience into a perfect movie night, without the need for expensive or complicated software.' }
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
197
|
+
'@context': 'https://schema.org',
|
|
198
|
+
'@type': 'FAQPage',
|
|
199
|
+
mainEntity: faq.map((item) => ({
|
|
200
|
+
'@type': 'Question',
|
|
201
|
+
name: item.question,
|
|
202
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
203
|
+
})),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const howToSchema: WithContext<HowTo> = {
|
|
207
|
+
'@context': 'https://schema.org',
|
|
208
|
+
'@type': 'HowTo',
|
|
209
|
+
name: title,
|
|
210
|
+
description,
|
|
211
|
+
step: howTo.map((step) => ({
|
|
212
|
+
'@type': 'HowToStep',
|
|
213
|
+
name: step.name,
|
|
214
|
+
text: step.text,
|
|
215
|
+
})),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
219
|
+
'@context': 'https://schema.org',
|
|
220
|
+
'@type': 'SoftwareApplication',
|
|
221
|
+
name: title,
|
|
222
|
+
description,
|
|
223
|
+
applicationCategory: 'UtilitiesApplication',
|
|
224
|
+
operatingSystem: 'Web',
|
|
225
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
226
|
+
inLanguage: 'en',
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export const content: SubtitleSyncLocaleContent = {
|
|
230
|
+
slug,
|
|
231
|
+
title,
|
|
232
|
+
description,
|
|
233
|
+
ui,
|
|
234
|
+
seo,
|
|
235
|
+
faq,
|
|
236
|
+
faqTitle: 'Frequently Asked Questions about Subtitle Synchronization',
|
|
237
|
+
bibliography,
|
|
238
|
+
bibliographyTitle: 'Technical Resources on Subtitle Formats',
|
|
239
|
+
howTo,
|
|
240
|
+
schemas: [faqSchema as any, howToSchema as any, appSchema],
|
|
241
|
+
};
|