diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..a171cde
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,29 @@
+# DocuTranslate 环境变量配置示例
+# 复制此文件为 .env 并按需修改(需配合 python-dotenv 或容器环境变量使用)
+
+# --- 代理配置 ---
+# 是否启用系统代理,设置为 true 开启
+# DOCUTRANSLATE_PROXY_ENABLED=true
+
+# --- 缓存配置 ---
+# 任务缓存数量(默认 10)
+# DOCUTRANSLATE_CACHE_NUM=10
+
+# --- 翻译 API 默认配置 ---
+# 前端"自定义接口-default"平台的默认值,留空则不预填
+
+# API 地址(Base URL),例如 https://api.openai.com/v1
+DOCUTRANSLATE_BASE_URL=
+
+# API 密钥
+DOCUTRANSLATE_API_KEY=
+
+# 模型 ID,例如 qwen-mt-turbo
+DOCUTRANSLATE_MODEL_ID=
+
+# --- 限流配置 ---
+# RPM 限制(每分钟请求数),留空则不限制
+DOCUTRANSLATE_RPM=
+
+# TPM 限制(每分钟 Token 数),留空则不限制
+DOCUTRANSLATE_TPM=
diff --git a/.gitignore b/.gitignore
index fa4a038..22be5fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,7 @@ docutranslate/output/
#idea
.idea/
#claude
-.claude/
\ No newline at end of file
+.claude/
+/.omc/
+# Environment variables
+.env
diff --git a/docutranslate/agents/agent.py b/docutranslate/agents/agent.py
index 762361b..9b2ceb5 100644
--- a/docutranslate/agents/agent.py
+++ b/docutranslate/agents/agent.py
@@ -59,6 +59,7 @@ class AgentConfig:
rpm: int | None = None # 每分钟请求数限制
tpm: int | None = None # 每分钟Token数限制
provider: ProviderType | None = None
+ source_lang: str | None = None # qwen-mt: 源语言
class TotalErrorCounter:
@@ -534,9 +535,10 @@ class Agent:
self.provider = config.provider if config.provider is not None else get_provider_by_domain(self.domain)
self.is_mt_mode = "mt" in self.model_id.lower()
- self.mt_source_lang = getattr(config, "source_lang", "auto")
+ self.mt_source_lang = config.source_lang if config.source_lang else "auto"
self.mt_target_lang = getattr(config, "to_lang", None)
self.mt_domains = getattr(config, "custom_prompt", None)
+ self.mt_glossary_dict = getattr(config, "glossary_dict", None)
def _estimate_tokens(self, text: str) -> int:
"""
@@ -588,7 +590,7 @@ class Agent:
return _MT_LANG_ALIASES[key]
return lang_text
- def _build_mt_translation_options(self) -> dict:
+ def _build_mt_translation_options(self, prompt: str = "") -> dict:
translation_options = {}
source_lang = self._normalize_mt_lang(self.mt_source_lang)
@@ -603,6 +605,15 @@ class Agent:
if domains:
translation_options["domains"] = domains
+ if self.mt_glossary_dict:
+ terminology_list = [
+ {"source": src, "target": tgt}
+ for src, tgt in self.mt_glossary_dict.items()
+ if src and tgt and src.lower() in prompt.lower()
+ ]
+ if terminology_list:
+ translation_options["terms"] = terminology_list
+
return translation_options
def _build_mt_user_prompt(self, prompt: str, system_prompt: str) -> str:
@@ -627,7 +638,7 @@ class Agent:
{"role": "user", "content": self._build_mt_user_prompt(prompt, system_prompt)},
],
}
- translation_options = self._build_mt_translation_options()
+ translation_options = self._build_mt_translation_options(prompt=prompt)
if translation_options:
data["translation_options"] = translation_options
return headers, data
diff --git a/docutranslate/app.py b/docutranslate/app.py
index fe943af..091d1d3 100644
--- a/docutranslate/app.py
+++ b/docutranslate/app.py
@@ -2472,6 +2472,25 @@ async def service_flat_translate(
})
+@app.get("/api/config", tags=["Config"], summary="获取服务端环境变量默认配置")
+async def get_config():
+ """返回由服务端环境变量预设的前端默认配置,不含敏感信息(API key 仅返回是否存在)。"""
+ from docutranslate.environment import (
+ DOCUTRANSLATE_BASE_URL,
+ DOCUTRANSLATE_API_KEY,
+ DOCUTRANSLATE_MODEL_ID,
+ DOCUTRANSLATE_RPM,
+ DOCUTRANSLATE_TPM,
+ )
+ return JSONResponse({
+ "base_url": DOCUTRANSLATE_BASE_URL,
+ "api_key": DOCUTRANSLATE_API_KEY,
+ "model_id": DOCUTRANSLATE_MODEL_ID,
+ "rpm": DOCUTRANSLATE_RPM,
+ "tpm": DOCUTRANSLATE_TPM,
+ })
+
+
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def main_page():
index_path = Path(STATIC_DIR) / "index.html"
diff --git a/docutranslate/core/factory.py b/docutranslate/core/factory.py
index 38bd7dc..79e3d19 100644
--- a/docutranslate/core/factory.py
+++ b/docutranslate/core/factory.py
@@ -60,7 +60,7 @@ def create_workflow_from_payload(payload: TranslatePayload, logger: logging.Logg
# 1. Markdown Based Workflow
if isinstance(payload, MarkdownWorkflowParams):
translator_args = payload.model_dump(
- include={"skip_translate", "base_url", "api_key", "model_id", "to_lang", "custom_prompt",
+ include={"skip_translate", "base_url", "api_key", "model_id", "to_lang", "source_lang", "custom_prompt",
"temperature", "thinking", "chunk_size", "concurrent", "glossary_dict", "timeout",
"retry", "system_proxy_enable", "force_json", "rpm", "tpm", "provider"},
exclude_none=True,
diff --git a/docutranslate/core/schemas.py b/docutranslate/core/schemas.py
index e734513..4b3ddf8 100644
--- a/docutranslate/core/schemas.py
+++ b/docutranslate/core/schemas.py
@@ -157,6 +157,9 @@ class BaseWorkflowParams(BaseModel):
custom_prompt: Optional[str] = Field(
default="", description="用户自定义的翻译Prompt。", alias="custom_prompt"
)
+ source_lang: Optional[str] = Field(
+ default=None, description="源语言(qwen-mt系列模型专用,如 'Chinese'、'English')。", examples=[None]
+ )
glossary_dict: Optional[Dict[str, str]] = Field(
None, description="术语表字典,key为原文,value为译文。", examples=[None]
)
diff --git a/docutranslate/environment.py b/docutranslate/environment.py
new file mode 100644
index 0000000..d624630
--- /dev/null
+++ b/docutranslate/environment.py
@@ -0,0 +1,41 @@
+# SPDX-FileCopyrightText: 2025 QinHan
+# SPDX-License-Identifier: MPL-2.0
+"""
+集中管理所有环境变量。
+所有 os.getenv() 调用应在此处统一声明,其他模块从这里导入。
+"""
+import os
+
+from dotenv import load_dotenv
+
+load_dotenv() # 自动从项目根目录的 .env 文件加载环境变量(不覆盖已有的 shell 变量)
+
+
+# --- 代理配置 ---
+# 是否启用系统代理,设置为 "true" 开启
+DOCUTRANSLATE_PROXY_ENABLED: bool = (
+ os.getenv("DOCUTRANSLATE_PROXY_ENABLED", "").lower() == "true"
+)
+
+# --- 缓存配置 ---
+# 任务缓存数量
+DOCUTRANSLATE_CACHE_NUM: int = int(os.getenv("DOCUTRANSLATE_CACHE_NUM", "10"))
+
+# --- 翻译 API 默认配置 ---
+# 默认 API 地址 (自定义接口的 Base URL)
+DOCUTRANSLATE_BASE_URL: str = os.getenv("DOCUTRANSLATE_BASE_URL", "")
+
+# 默认 API 密钥
+DOCUTRANSLATE_API_KEY: str = os.getenv("DOCUTRANSLATE_API_KEY", "")
+
+# 默认模型 ID
+DOCUTRANSLATE_MODEL_ID: str = os.getenv("DOCUTRANSLATE_MODEL_ID", "")
+
+# --- 限流默认配置 ---
+# 默认 RPM 限制 (Requests Per Minute),不设置则不限制
+_rpm_str = os.getenv("DOCUTRANSLATE_RPM", "")
+DOCUTRANSLATE_RPM: int | None = int(_rpm_str) if _rpm_str.strip() else None
+
+# 默认 TPM 限制 (Tokens Per Minute),不设置则不限制
+_tpm_str = os.getenv("DOCUTRANSLATE_TPM", "")
+DOCUTRANSLATE_TPM: int | None = int(_tpm_str) if _tpm_str.strip() else None
diff --git a/docutranslate/global_values/__init__.py b/docutranslate/global_values/__init__.py
index 3df60cf..c42c229 100644
--- a/docutranslate/global_values/__init__.py
+++ b/docutranslate/global_values/__init__.py
@@ -1,16 +1,9 @@
# SPDX-FileCopyrightText: 2025 QinHan
# SPDX-License-Identifier: MPL-2.0
-import os
+from docutranslate.environment import DOCUTRANSLATE_PROXY_ENABLED
from .conditional_import import available_packages, conditional_import
-USE_PROXY = (
- True
- if (
- os.getenv("DOCUTRANSLATE_PROXY_ENABLED")
- and os.getenv("DOCUTRANSLATE_PROXY_ENABLED").lower() == "true"
- )
- else False
-)
+USE_PROXY = DOCUTRANSLATE_PROXY_ENABLED
if USE_PROXY:
print(f"USE_PROXY:{USE_PROXY}")
diff --git a/docutranslate/sdk.py b/docutranslate/sdk.py
index a354c8b..566054e 100644
--- a/docutranslate/sdk.py
+++ b/docutranslate/sdk.py
@@ -198,6 +198,7 @@ class Client:
retry: Optional[int] = None,
thinking: Optional[ThinkingMode] = None,
custom_prompt: Optional[str] = None,
+ source_lang: Optional[str] = None,
system_proxy_enable: Optional[bool] = None,
force_json: Optional[bool] = None,
rpm: Optional[int] = None,
@@ -264,6 +265,7 @@ class Client:
retry: Optional[int] = None,
thinking: Optional[ThinkingMode] = None,
custom_prompt: Optional[str] = None,
+ source_lang: Optional[str] = None,
system_proxy_enable: Optional[bool] = None,
force_json: Optional[bool] = None,
rpm: Optional[int] = None,
diff --git a/docutranslate/static/index.html b/docutranslate/static/index.html
index 87de12d..4a307e8 100644
--- a/docutranslate/static/index.html
+++ b/docutranslate/static/index.html
@@ -596,6 +596,7 @@
+
@@ -673,7 +674,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1447,25 +1306,6 @@
retry: 3,
rpm: null, // New RPM
tpm: null, // New TPM
- glossary_generate_enable: false,
- glossary_agent_custom_prompt: '',
- glossary_agent_config_choice: 'same',
- glossary_agent_platform: 'https://api.302.ai/v1',
- glossary_agent_baseurl: '',
- glossary_agent_key: '',
- glossary_agent_model_id: '',
- glossary_agent_provider: 'api.openai.com', // Default glossary provider
- glossary_agent_to_lang: 'Simplified Chinese',
- glossary_agent_custom_to_lang: '',
- glossary_agent_chunk_size: 1000,
- glossary_agent_concurrent: 5,
- glossary_agent_temperature: 0.7,
- glossary_agent_retry: 3,
- glossary_agent_thinking: 'default',
- glossary_agent_system_proxy_enable: false,
- glossary_agent_force_json: false,
- glossary_agent_rpm: null, // New Glossary Agent RPM
- glossary_agent_tpm: null // New Glossary Agent TPM
});
// Nested Params for specific workflows
@@ -1497,7 +1337,7 @@
return (v === null || v === '' || v === 'null') ? null : Number(v);
};
- form.workflow_type = get('translator_last_workflow', 'markdown_based');
+ form.workflow_type = get('translator_last_workflow', 'docx');
form.auto_workflow_enabled = getBool('translator_auto_workflow_enabled', true);
form.convert_engine = get('translator_convert_engin', 'mineru');
form.mineru_token = get('translator_mineru_token', '');
@@ -1516,12 +1356,12 @@
form.formula_ocr = getBool('translator_formula_ocr', true);
form.code_ocr = getBool('translator_code_ocr', true);
form.skip_translate = getBool('translator_skip_translate', false);
- form.platform = get('translator_platform_last_platform', 'https://api.302.ai/v1');
+ form.platform = get('translator_platform_last_platform', 'custom');
form.system_proxy_enable = getBool('translator_system_proxy_enable', false);
form.force_json = getBool('translator_force_json', false);
form.to_lang = get('translator_to_lang', 'Simplified Chinese');
form.custom_to_lang = get('translator_custom_to_lang', '');
- form.thinking = get('translator_thinking_mode', 'disable');
+ form.thinking = get('translator_thinking_mode', 'default');
form.custom_prompt = get('custom_prompt', '');
form.chunk_size = getNum('chunk_size', 1000);
form.concurrent = getNum('concurrent', 5);
@@ -1539,30 +1379,6 @@
form.provider = platObj ? platObj.provider : '';
}
- form.glossary_generate_enable = getBool('glossary_generate_enable', false);
- form.glossary_agent_custom_prompt = get('glossary_agent_custom_prompt', '');
- form.glossary_agent_config_choice = get('glossary_agent_config_choice', 'same');
- form.glossary_agent_platform = get('glossary_agent_platform_last_platform', 'https://api.302.ai/v1');
- form.glossary_agent_to_lang = get('glossary_agent_to_lang', 'Simplified Chinese');
- form.glossary_agent_custom_to_lang = get('glossary_agent_custom_to_lang', '');
- form.glossary_agent_chunk_size = getNum('glossary_agent_chunk_size', 1000);
- form.glossary_agent_concurrent = getNum('glossary_agent_concurrent', 5);
- form.glossary_agent_temperature = getNum('glossary_agent_temperature', 0.7);
- form.glossary_agent_retry = getNum('glossary_agent_retry', 3);
- form.glossary_agent_thinking = get('glossary_agent_thinking_mode', 'default');
- form.glossary_agent_system_proxy_enable = getBool('glossary_agent_system_proxy_enable', false);
- form.glossary_agent_force_json = getBool('glossary_agent_force_json', false);
- form.glossary_agent_rpm = getNumOrNull('glossary_agent_rpm'); // Load Glossary RPM
- form.glossary_agent_tpm = getNumOrNull('glossary_agent_tpm'); // Load Glossary TPM
-
- // Determine Glossary Provider
- const gPlatObj = KNOWN_PLATFORMS.find(p => p.val === form.glossary_agent_platform);
- if (form.glossary_agent_platform === 'custom') {
- form.glossary_agent_provider = get('glossary_agent_platform_custom_provider', 'default');
- } else {
- form.glossary_agent_provider = gPlatObj ? gPlatObj.provider : '';
- }
-
// Restore workflow specific params
['txt', 'xlsx', 'docx', 'srt', 'epub', 'html', 'ass', 'pptx'].forEach(t => {
workflowParams[t].insert_mode = get(`translator_${t}_insert_mode`, 'replace');
@@ -1574,7 +1390,6 @@
// Trigger platform updates to load API keys/models
updatePlatformParams(form.platform, 'translator_platform', form);
- updatePlatformParams(form.glossary_agent_platform, 'glossary_agent_platform', form, true);
};
// --- 新增:专门用于将当前 form 数据全部写入 localStorage 的函数 ---
@@ -1626,30 +1441,7 @@
s('translator_provider', f.provider);
if (f.platform === 'custom') s('translator_platform_custom_base_url', f.base_url);
- // 2. 术语表相关
- s('glossary_generate_enable', f.glossary_generate_enable);
- s('glossary_agent_custom_prompt', f.glossary_agent_custom_prompt);
- s('glossary_agent_config_choice', f.glossary_agent_config_choice);
- s('glossary_agent_platform_last_platform', f.glossary_agent_platform);
- s('glossary_agent_to_lang', f.glossary_agent_to_lang);
- s('glossary_agent_custom_to_lang', f.glossary_agent_custom_to_lang);
- s('glossary_agent_chunk_size', f.glossary_agent_chunk_size);
- s('glossary_agent_concurrent', f.glossary_agent_concurrent);
- s('glossary_agent_temperature', f.glossary_agent_temperature);
- s('glossary_agent_retry', f.glossary_agent_retry);
- s('glossary_agent_thinking_mode', f.glossary_agent_thinking);
- s('glossary_agent_system_proxy_enable', f.glossary_agent_system_proxy_enable);
- s('glossary_agent_force_json', f.glossary_agent_force_json);
- s('glossary_agent_rpm', f.glossary_agent_rpm || '');
- s('glossary_agent_tpm', f.glossary_agent_tpm || '');
-
- // 术语表平台 Key
- s(`glossary_agent_platform_${f.glossary_agent_platform}_apikey`, f.glossary_agent_key);
- s(`glossary_agent_platform_${f.glossary_agent_platform}_model_id`, f.glossary_agent_model_id);
- s('glossary_agent_provider', f.glossary_agent_provider);
- if (f.glossary_agent_platform === 'custom') s('glossary_agent_platform_custom_base_url', f.glossary_agent_baseurl);
-
- // 3. 自动循环保存所有具体工作流参数 (txt, docx, xlsx...)
+ // 2. 自动循环保存所有具体工作流参数 (txt, docx, xlsx...)
for (const [wfType, params] of Object.entries(workflowParams)) {
for (const [key, val] of Object.entries(params)) {
s(`translator_${wfType}_${key}`, val);
@@ -1657,27 +1449,17 @@
}
};
- const updatePlatformParams = (plat, prefix, target, isGlossary = false) => {
+ const updatePlatformParams = (plat, prefix, target) => {
const get = (k) => localStorage.getItem(k) || '';
- if (isGlossary) {
- target.glossary_agent_key = get(`${prefix}_${plat}_apikey`);
- target.glossary_agent_model_id = get(`${prefix}_${plat}_model_id`);
- target.glossary_agent_baseurl = plat === 'custom' ? get(`${prefix}_custom_base_url`) : plat;
- } else {
- target.api_key = get(`${prefix}_${plat}_apikey`);
- target.model_id = get(`${prefix}_${plat}_model_id`);
- target.base_url = plat === 'custom' ? get(`${prefix}_custom_base_url`) : plat;
- }
+ target.api_key = get(`${prefix}_${plat}_apikey`);
+ target.model_id = get(`${prefix}_${plat}_model_id`);
+ target.base_url = plat === 'custom' ? get(`${prefix}_custom_base_url`) : plat;
};
watch(() => form.platform, (n) => {
updatePlatformParams(n, 'translator_platform', form);
});
- watch(() => form.glossary_agent_platform, (n) => {
- updatePlatformParams(n, 'glossary_agent_platform', form, true);
- });
-
const t = (k) => {
const dict = i18nData.value[currentLang.value] || i18nData.value['zh'] || {};
return dict[k] || k;
@@ -1751,12 +1533,11 @@
// Dynamic Step Numbering
const stepMap = computed(() => {
let step = 2;
- const map = {specific: 0, parsing: 0, ai: 0, trans: 0, glossary: 0};
+ const map = {specific: 0, parsing: 0, ai: 0, trans: 0};
if (currentWorkflowConfig.value) map.specific = step++;
if (form.workflow_type === 'markdown_based') map.parsing = step++;
map.ai = step++;
if (!form.skip_translate) map.trans = step++;
- map.glossary = step++;
return map;
});
@@ -1875,33 +1656,11 @@
glossary_dict: Object.keys(glossaryData.value).length ? glossaryData.value : null,
system_proxy_enable: form.system_proxy_enable,
force_json: form.force_json,
- glossary_generate_enable: form.glossary_generate_enable,
workflow_type: form.workflow_type,
rpm: emptyToNull(form.rpm),
tpm: emptyToNull(form.tpm)
};
- // Agent Config
- if (basePayload.glossary_generate_enable) {
- const isCustom = form.glossary_agent_config_choice === 'custom';
- basePayload.glossary_agent_config = {
- base_url: isCustom ? emptyToNull(form.glossary_agent_baseurl) : basePayload.base_url,
- api_key: isCustom ? (form.glossary_agent_key || "") : basePayload.api_key,
- model_id: isCustom ? emptyToNull(form.glossary_agent_model_id) : basePayload.model_id,
- provider: isCustom ? emptyToNull(form.glossary_agent_provider) : basePayload.provider, // Add provider
- to_lang: isCustom ? (form.glossary_agent_to_lang === 'custom' ? form.glossary_agent_custom_to_lang : form.glossary_agent_to_lang) : basePayload.to_lang,
- custom_prompt: emptyToNull(form.glossary_agent_custom_prompt),
- temperature: isCustom ? Number(form.glossary_agent_temperature) : basePayload.temperature,
- concurrent: isCustom ? Number(form.glossary_agent_concurrent) : basePayload.concurrent,
- retry: isCustom ? Number(form.glossary_agent_retry) : basePayload.retry,
- thinking: isCustom ? form.glossary_agent_thinking : basePayload.thinking,
- system_proxy_enable: isCustom ? form.glossary_agent_system_proxy_enable : basePayload.system_proxy_enable,
- chunk_size: isCustom ? Number(form.glossary_agent_chunk_size) : basePayload.chunk_size,
- force_json: isCustom ? form.glossary_agent_force_json : basePayload.force_json,
- rpm: isCustom ? emptyToNull(form.glossary_agent_rpm) : basePayload.rpm,
- tpm: isCustom ? emptyToNull(form.glossary_agent_tpm) : basePayload.tpm
- };
- }
// Specific Workflow Params
if (form.workflow_type === 'markdown_based') {
@@ -2377,13 +2136,31 @@
// Backend Metadata
try {
- const [metaRes, enginRes, paramsRes] = await Promise.all([
- fetch("/service/meta"), fetch('/service/engin-list'), fetch("/service/default-params")
+ const [metaRes, enginRes, paramsRes, configRes] = await Promise.all([
+ fetch("/service/meta"), fetch('/service/engin-list'), fetch("/service/default-params"),
+ fetch("/api/config")
]);
const meta = await metaRes.json();
version.value = meta.version;
enginList.value = await enginRes.json();
Object.assign(defaultParams, await paramsRes.json());
+ const envConfig = await configRes.json().catch(() => ({}));
+ // 将服务端环境变量作为 localStorage 未设置时的回退默认值
+ if (envConfig.base_url && !localStorage.getItem('translator_platform_custom_base_url')) {
+ localStorage.setItem('translator_platform_custom_base_url', envConfig.base_url);
+ }
+ if (envConfig.api_key && !localStorage.getItem('translator_platform_custom_apikey')) {
+ localStorage.setItem('translator_platform_custom_apikey', envConfig.api_key);
+ }
+ if (envConfig.model_id && !localStorage.getItem('translator_platform_custom_model_id')) {
+ localStorage.setItem('translator_platform_custom_model_id', envConfig.model_id);
+ }
+ if (envConfig.rpm != null && !localStorage.getItem('rpm')) {
+ localStorage.setItem('rpm', String(envConfig.rpm));
+ }
+ if (envConfig.tpm != null && !localStorage.getItem('tpm')) {
+ localStorage.setItem('tpm', String(envConfig.tpm));
+ }
} catch (e) {
console.error("Backend init failed", e);
}
diff --git a/docutranslate/translator/ai_translator/md_translator.py b/docutranslate/translator/ai_translator/md_translator.py
index 5b927eb..295c9f4 100644
--- a/docutranslate/translator/ai_translator/md_translator.py
+++ b/docutranslate/translator/ai_translator/md_translator.py
@@ -31,6 +31,7 @@ class MDTranslator(AiTranslator):
if not self.skip_translate:
agent_config = MDTranslateAgentConfig(custom_prompt=config.custom_prompt,
to_lang=config.to_lang,
+ source_lang=config.source_lang,
base_url=config.base_url,
api_key=config.api_key,
model_id=config.model_id,
diff --git a/pyproject.toml b/pyproject.toml
index 52a13b6..010ba91 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,6 +22,7 @@ dependencies = [
"pypdf>=6.4.2",
"regex>=2025.11.3",
"charset-normalizer>=3.4.4",
+ "python-dotenv>=1.0.0",
]
dynamic = ["version"]
diff --git a/uv.lock b/uv.lock
index ff2874d..402c3a1 100644
--- a/uv.lock
+++ b/uv.lock
@@ -383,6 +383,7 @@ dependencies = [
{ name = "pypdf" },
{ name = "pysubs2" },
{ name = "python-docx" },
+ { name = "python-dotenv" },
{ name = "python-pptx" },
{ name = "regex" },
{ name = "srt" },
@@ -422,6 +423,7 @@ requires-dist = [
{ name = "pypdf", specifier = ">=6.4.2" },
{ name = "pysubs2", specifier = ">=1.8.0" },
{ name = "python-docx", specifier = ">=1.2.0" },
+ { name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "python-pptx", specifier = ">=1.0.2" },
{ name = "regex", specifier = ">=2025.11.3" },
{ name = "srt", specifier = ">=3.5.3" },
@@ -1738,14 +1740,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 },
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 },
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 },
- { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441 },
- { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291 },
- { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632 },
- { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905 },
- { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 },
- { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 },
- { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 },
- { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 },
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 },
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 },
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 },