Skip to content

Commit 0d61bd8

Browse files
hydropixclaude
andcommitted
feat: expand output filename placeholders with {model}, {date}, {datetime}
Adds support for {sourceLang}, {model}, {date}, {datetime} in the web UI naming convention, and regenerates the output filename at translation time so A/B testing the same file across different models produces distinct, identifiable output files. Model names are sanitized for cross-platform filename safety (slashes, colons, etc.). Closes #129 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent edf0242 commit 0d61bd8

5 files changed

Lines changed: 89 additions & 29 deletions

File tree

.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ HOST=127.0.0.1 # Server host (127.0.0.1 for localhost only, 0.0.0.0 for all net
1717
OUTPUT_DIR=translated_files # Directory for translated output files
1818

1919
# Output filename pattern (naming convention)
20-
# Use {originalName}, {targetLang}, {sourceLang}, {model}, {ext} as placeholders
20+
# Placeholders: {originalName}, {targetLang}, {sourceLang}, {model}, {date}, {datetime}, {ext}
21+
# {date} → YYYY-MM-DD
22+
# {datetime} → YYYY-MM-DD_HH-MM-SS
23+
# {model} → sanitized (e.g., "anthropic/claude-4.5-haiku" → "anthropic_claude-4.5-haiku")
24+
# Example for model A/B testing: {originalName} ({targetLang}) ({model})_{date}.{ext}
2125
OUTPUT_FILENAME_PATTERN={originalName} ({targetLang}).{ext}
2226

2327
# LLM Provider Settings

src/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@
245245
OUTPUT_DIR = os.getenv('OUTPUT_DIR', 'translated_files')
246246

247247
# Output filename pattern
248-
# Use {originalName}, {targetLang}, {sourceLang}, {model}, {ext} as placeholders
248+
# Placeholders: {originalName}, {targetLang}, {sourceLang}, {model}, {date}, {datetime}, {ext}
249249
OUTPUT_FILENAME_PATTERN = os.getenv('OUTPUT_FILENAME_PATTERN', '{originalName} ({targetLang}).{ext}')
250250

251251
# Debug mode (reload after .env is loaded)

src/web/static/js/files/file-upload.js

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,73 @@ const FILE_QUEUE_STORAGE_KEY = 'tbl_file_queue';
1717
let lastUploadedFileName = null;
1818

1919
/**
20-
* Generate output filename based on pattern
21-
* @param {File} file - Original file
22-
* @param {string} pattern - Output pattern (e.g., "{originalName} ({targetLang}).{ext}")
20+
* Sanitize a value to be safe in a filename across Windows/macOS/Linux.
21+
* Strips path separators and reserved chars, collapses whitespace.
22+
*/
23+
function sanitizeFilenamePart(value) {
24+
if (value === null || value === undefined) return '';
25+
return String(value)
26+
.replace(/[\/\\:*?"<>|]+/g, '_')
27+
.replace(/\s+/g, ' ')
28+
.trim();
29+
}
30+
31+
/**
32+
* Resolve a language value from a <select> + custom <input> pair,
33+
* falling back to 'Translated' / 'Source' when empty.
34+
*/
35+
function resolveLanguageValue(selectId, customId, fallback) {
36+
const selectEl = DomHelpers.getElement(selectId);
37+
const customEl = DomHelpers.getElement(customId);
38+
let value = selectEl?.value || '';
39+
if (value === 'Other') {
40+
value = customEl?.value?.trim() || '';
41+
}
42+
return value || fallback;
43+
}
44+
45+
/**
46+
* Generate output filename based on pattern.
47+
* Supported placeholders: {originalName}, {targetLang}, {sourceLang}, {model}, {date}, {datetime}, {ext}
48+
*
49+
* @param {File|{name: string}} file - Original file (only .name is used)
50+
* @param {string} pattern - Output pattern
51+
* @param {Object} [overrides] - Optional explicit values that win over DOM lookups
52+
* @param {string} [overrides.sourceLang]
53+
* @param {string} [overrides.targetLang]
54+
* @param {string} [overrides.model]
2355
* @returns {string} Generated filename
2456
*/
25-
function generateOutputFilename(file, pattern) {
57+
export function generateOutputFilename(file, pattern, overrides = {}) {
2658
const fileExtension = file.name.split('.').pop().toLowerCase();
2759
const originalNameWithoutExt = file.name.replace(/\.[^/.]+$/, "");
2860

29-
// Get target language from the form
30-
const targetLangSelect = DomHelpers.getElement('targetLang');
31-
const customTargetLang = DomHelpers.getElement('customTargetLang');
32-
let targetLang = targetLangSelect?.value || '';
33-
34-
// If "Other" is selected, use the custom input value
35-
if (targetLang === 'Other') {
36-
targetLang = customTargetLang?.value?.trim() || '';
61+
const targetLang = overrides.targetLang
62+
|| resolveLanguageValue('targetLang', 'customTargetLang', 'Translated');
63+
const sourceLang = overrides.sourceLang
64+
|| resolveLanguageValue('sourceLang', 'customSourceLang', 'Source');
65+
const model = overrides.model ?? DomHelpers.getValue('model') ?? '';
66+
67+
const now = new Date();
68+
const pad = (n) => String(n).padStart(2, '0');
69+
const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
70+
const datetime = `${date}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
71+
72+
const replacements = {
73+
'{originalName}': sanitizeFilenamePart(originalNameWithoutExt),
74+
'{targetLang}': sanitizeFilenamePart(targetLang),
75+
'{sourceLang}': sanitizeFilenamePart(sourceLang),
76+
'{model}': sanitizeFilenamePart(model),
77+
'{date}': date,
78+
'{datetime}': datetime,
79+
'{ext}': fileExtension
80+
};
81+
82+
let result = pattern || '{originalName} ({targetLang}).{ext}';
83+
for (const [token, value] of Object.entries(replacements)) {
84+
result = result.split(token).join(value);
3785
}
38-
39-
// Use "Translated" as fallback if no language specified
40-
if (!targetLang) {
41-
targetLang = 'Translated';
42-
}
43-
44-
return pattern
45-
.replace("{originalName}", originalNameWithoutExt)
46-
.replace("{targetLang}", targetLang)
47-
.replace("{ext}", fileExtension);
86+
return result;
4887
}
4988

5089
/**

src/web/static/js/translation/batch-controller.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { Validators } from '../utils/validators.js';
1313
import { ApiKeyUtils } from '../utils/api-key-utils.js';
1414
import { StatusManager } from '../utils/status-manager.js';
1515
import { ProgressManager } from './progress-manager.js';
16-
import { FileUpload } from '../files/file-upload.js';
16+
import { FileUpload, generateOutputFilename } from '../files/file-upload.js';
1717
import { TranslationTracker } from './translation-tracker.js';
1818

1919
/**
@@ -39,6 +39,23 @@ function getTranslationConfig(file) {
3939
const targetLanguageVal = file.targetLanguage;
4040

4141
const provider = DomHelpers.getValue('llmProvider');
42+
const currentModel = DomHelpers.getValue('model') || '';
43+
44+
// Regenerate output filename at translation time so placeholders like
45+
// {model}, {date}, {datetime} reflect the current run. The value stored
46+
// on the file object was computed at upload time and may be stale,
47+
// especially when the same file is re-translated with a different model.
48+
const outputPattern = DomHelpers.getValue('outputFilenamePattern')
49+
|| '{originalName} ({targetLang}).{ext}';
50+
const resolvedOutputFilename = generateOutputFilename(
51+
{ name: file.name },
52+
outputPattern,
53+
{
54+
sourceLang: sourceLanguageVal,
55+
targetLang: targetLanguageVal,
56+
model: currentModel
57+
}
58+
);
4259

4360
const promptOptions = {
4461
preserve_technical_content: true,
@@ -53,7 +70,7 @@ function getTranslationConfig(file) {
5370
const config = {
5471
source_language: sourceLanguageVal,
5572
target_language: targetLanguageVal,
56-
model: DomHelpers.getValue('model'),
73+
model: currentModel,
5774
llm_api_endpoint: provider === 'openai' ?
5875
DomHelpers.getValue('openaiEndpoint') :
5976
DomHelpers.getValue('apiEndpoint'),
@@ -66,7 +83,7 @@ function getTranslationConfig(file) {
6683
poe_api_key: provider === 'poe' ? ApiKeyUtils.getValue('poeApiKey') : '',
6784
nim_api_key: provider === 'nim' ? ApiKeyUtils.getValue('nimApiKey') : '',
6885
input_filename: file.name,
69-
output_filename: file.outputFilename,
86+
output_filename: resolvedOutputFilename,
7087
file_type: file.fileType,
7188
prompt_options: promptOptions,
7289
bilingual_output: DomHelpers.getElement('bilingualMode')?.checked || false,

src/web/templates/translation_interface.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,9 +333,9 @@ <h3>Drop files to translate</h3>
333333
<div class="form-group" style="margin-bottom: 15px;">
334334
<label>Naming Convention</label>
335335
<div class="neu-inset-light">
336-
<input type="text" class="form-control" id="outputFilenamePattern" value="{originalName} ({targetLang}).{ext}" title="Use {originalName}, {targetLang} as placeholders">
336+
<input type="text" class="form-control" id="outputFilenamePattern" value="{originalName} ({targetLang}).{ext}" title="Use {originalName}, {targetLang}, {sourceLang}, {model}, {date}, {datetime}, {ext} as placeholders">
337337
</div>
338-
<small style="color: var(--text-muted-light); font-size: 0.75rem; margin-top: 0.5rem; display: block;">Placeholders: {originalName}, {targetLang}, {ext} — Example: Book (French).epub</small>
338+
<small style="color: var(--text-muted-light); font-size: 0.75rem; margin-top: 0.5rem; display: block;">Placeholders: {originalName}, {targetLang}, {sourceLang}, {model}, {date}, {datetime}, {ext} — Example: {originalName} ({targetLang}) ({model})_{date}.{ext}</small>
339339
</div>
340340

341341
<!-- Save Settings Button -->

0 commit comments

Comments
 (0)