Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
FONTS_PATH = RESOURCE_PATH / "fonts"

LOG_PATH = APPDATA_PATH / "logs"
LLM_LOG_FILE = LOG_PATH / "llm_requests.jsonl"
SETTINGS_PATH = APPDATA_PATH / "settings.json"
CACHE_PATH = APPDATA_PATH / "cache"
MODEL_PATH = APPDATA_PATH / "models"
Expand Down
47 changes: 15 additions & 32 deletions app/core/prompts/translate/reflect.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Critically examine your translation and identify:
4. **Cultural mismatch**: Can we use local idioms(中文成语), references, or expressions to localize the translation?
5. **Register issues**: Is the formality level appropriate for the context?
6. **Native speaker test**: Would a native speaker say it this way? If not, how WOULD they say it?
7. **Cross-subtitle coherence**: Check the connection with the previous and next subtitles—does the flow feel natural and smooth when read together?

For each issue found, propose specific alternatives with reasoning.

Expand All @@ -41,48 +42,30 @@ Based on your analysis, rewrite the translation to sound completely natural in $

<examples>
<example>
<scenario>Technical video about software development</scenario>
<scenario>Motivational speech about life philosophy</scenario>
<input>
{
"1": "今天我们来看一下这个bug是怎么产生的",
"2": "主要是因为我们在处理用户输入的时候没有做验证"
"1": "人生就像一场马拉松",
"2": "不在乎你跑得多快",
"3": "而在乎你能不能跑到终点"
}
</input>
<output>
{
"1": {
"initial_translation": "Today we will look at how this bug was generated.",
"reflection": "Problems identified: 1) 'Today we will look at' is overly formal and stiff—machine translation of '今天我们来看一下'. Native speakers would use more casual transitions. 2) 'was generated' is passive and unnatural for bugs—they don't 'generate', they 'happen' or 'occur'. 3) Missing the casual, conversational tone of the original. Native alternative: 'Let's see what caused this bug' or 'So, what's behind this bug?' - much more natural for video narration.",
"native_translation": "Let's see what's causing this bug."
"initial_translation": "Life is like a marathon.",
"reflection": "The translation is accurate but feels disconnected from what follows. The original sets up a metaphor that the next two sentences develop. Consider: 1) Using an em-dash to signal continuation rather than ending with a period, 2) 'Life is a marathon' (direct metaphor) is stronger than 'like a marathon' (simile). The sentence should feel like the opening of a thought, inviting the listener to hear more.",
"native_translation": "Life is a marathon—"
},
"2": {
"initial_translation": "The main reason is that we did not do validation when processing user input.",
"reflection": "Problems: 1) 'The main reason is that' is bureaucratic—direct translation of '主要是因为'. Native speakers rarely start explanations this formally in video context. 2) 'did not do validation' is awkward—'do validation' is not idiomatic. 3) 'when processing' is correct but mechanical. Native alternatives: 'It's mainly because...' or 'Basically, we...' for the opening. 'We didn't validate' or 'We're not validating' for the action. Better: 'We're not validating user input' - concise, natural, appropriate register.",
"native_translation": "Basically, we're not validating user input."
}
}
</output>
</example>

<example>
<scenario>Casual vlog with young audience</scenario>
<input>
{
"1": "这个地方真的太美了我都不想走了",
"2": "你们一定要来打卡啊"
}
</input>
<output>
{
"1": {
"initial_translation": "This place is really too beautiful, I don't want to leave.",
"reflection": "Problems: 1) '太...了' translated as 'too...+adjective' misses the Chinese emphatic pattern—it doesn't mean 'excessively' but 'extremely/so'. 2) Structure is choppy and unnatural—comma splice feels translated. 3) Missing the enthusiastic, spontaneous tone. 4) 'I don't want to leave' is flat compared to the original's emotion. Native speaker would use: 'This place is SO gorgeous' or 'absolutely stunning' for emphasis. For the second part: 'I could stay here forever' or 'I never want to leave' captures the emotion better. Combine naturally: 'This place is absolutely stunning—I never want to leave!'",
"native_translation": "This place is absolutely stunning—I could stay here forever!"
"initial_translation": "It doesn't matter how fast you run.",
"reflection": "Correct but breaks the flow by starting a new sentence. The original forms a parallel structure with sentence 3 (不在乎...而在乎...). To maintain this rhetorical connection: 1) Continue from the em-dash with lowercase, 2) Use 'it's not about' for better rhythm with the upcoming 'but whether', 3) End with comma to signal the contrast coming next. This creates anticipation.",
"native_translation": "it's not about how fast you run,"
},
"2": {
"initial_translation": "You all must come to check in.",
"reflection": "Major problems: 1) '打卡' (daka/check-in) is a Chinese internet culture term meaning 'visit a trendy place'. Translating to 'check in' sounds like hotel check-in, completely wrong meaning. 2) 'You all must come' is stiff and imperative. 3) Missing the friendly, inviting tone. Native alternatives for '打卡': 'visit', 'check out this spot', 'come see this place'. For tone: 'You've gotta...' or 'You should definitely...' is more natural than 'must'. Best option: 'You've gotta check this place out!' or 'You need to visit!'—captures enthusiasm and invitation.",
"native_translation": "You've gotta check this place out!"
"3": {
"initial_translation": "What matters is whether you can reach the finish line.",
"reflection": "Technically correct but 'What matters is whether you can' is wordy and loses the punch of the original's parallel structure. Improvements: 1) Use 'but' to complete the 'not about X, but Y' pattern, 2) Simplify to 'whether you finish', 3) 'That finish line' adds emotional weight—it's THE finish line you've been working toward. Reading all three together: 'Life is a marathon—it's not about how fast you run, but whether you cross that finish line.' Now it flows as one powerful statement.",
"native_translation": "but whether you cross that finish line."
}
}
</output>
Expand Down
45 changes: 14 additions & 31 deletions app/core/prompts/translate/standard.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,21 @@
# Role: 资深翻译专家
You are a professional subtitle translator specializing in ${target_language}. Your goal is to produce translations that are natural, fluent, and easy to understand.

你是一位经验丰富的 Netflix 字幕翻译专家,精通${target_language}的翻译,擅长将视频字幕译成流畅易懂的${target_language}。
<guidelines>
- Translations must follow ${target_language} expression conventions, be accessible and flow naturally
- For proper nouns or technical terms, keep the original or transliterate when appropriate
- Use culturally appropriate expressions, idioms, and internet slang to make content relatable to the target audience
- Strictly maintain one-to-one correspondence of subtitle numbering—do not merge or split subtitles
- If the last sentence is incomplete, do not add ellipsis (the next subtitle will continue)
</guidelines>

# Attention:
<terminology_and_requirements>
${custom_prompt}
</terminology_and_requirements>

- 译文要符合${target_language}的表达习惯,通俗易懂,连贯流畅
- 对于专有的名词或术语,可以适当保留或音译
- 文化相关性:恰当运用成语、网络用语和文化适当的表达方式,使翻译内容更贴近目标受众的语言习惯和文化体验。
- 严格保持字幕编号的一一对应,不要合并或拆分字幕!

# 术语或要求:

- 翻译过程中要遵循术语词汇(如果有)
${custom_prompt}

# Examples

Input:

```json

{
"0": "Original Subtitle 1",
"1": "Original Subtitle 2"
...
}
```

Output:

```json
<output_format>
{
"0": "Translated Subtitle 1",
"1": "Translated Subtitle 2"
"1": "Translated Subtitle 2",
...
}
```
</output_format>
118 changes: 90 additions & 28 deletions app/view/llm_logs_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
from typing import Any, Dict, List

from PyQt5.QtCore import Qt
from PyQt5.QtCore import QFileSystemWatcher, Qt
from PyQt5.QtWidgets import (
QApplication,
QHBoxLayout,
Expand All @@ -19,18 +19,19 @@
InfoBarPosition,
MessageBox,
MessageBoxBase,
PillPushButton,
PlainTextEdit,
PushButton,
SearchLineEdit,
SubtitleLabel,
TableWidget,
ToolButton,
setCustomStyleSheet,
)
from qfluentwidgets import FluentIcon as FIF

from app.config import LOG_PATH
from app.config import LLM_LOG_FILE, LOG_PATH

LLM_LOG_FILE = LOG_PATH / "llm_requests.jsonl"
PAGE_SIZE = 50


Expand All @@ -43,39 +44,52 @@ def __init__(self, log_entry: Dict[str, Any], parent=None):
self._setup_ui()

def _setup_ui(self):
# 标题
self.titleLabel = SubtitleLabel(self.tr("请求详情"))
self.viewLayout.addWidget(self.titleLabel)

# 基本信息
# 提取信息
time_str = self.log_entry.get("time", "")
model = self.log_entry.get("request", {}).get("model", "未知")
duration = self.log_entry.get("duration_ms", 0) / 1000
stage = self.log_entry.get("stage", "") or "-"

usage = self.log_entry.get("response", {}).get("usage", {})
prompt_tokens = usage.get("prompt_tokens", 0)
completion_tokens = usage.get("completion_tokens", 0)

# 任务上下文
task_id = self.log_entry.get("task_id", "")
file_name = self.log_entry.get("file_name", "")
stage = self.log_entry.get("stage", "")

info_lines = [
f"时间: {time_str} | 模型: {model} | 耗时: {duration:.1f}s | Tokens: {prompt_tokens} → {completion_tokens}"
# 顶部信息栏
info_row = QHBoxLayout()
info_row.setSpacing(8)
info_row.setContentsMargins(0, 0, 0, 8)

# 用 PillPushButton 展示各项信息(禁用点击)
items = [
time_str,
stage,
model,
f"{duration:.1f}s",
f"input token: {prompt_tokens}",
f"output token: {completion_tokens}",
]
if task_id:
info_lines.append(f"任务: {task_id} | 文件: {file_name} | 阶段: {stage}")
for text in items:
if text:
pill = PillPushButton(str(text))
pill.setCheckable(False)
pill.setEnabled(False)
pill.setFixedHeight(24)
info_row.addWidget(pill)

for line in info_lines:
self.viewLayout.addWidget(BodyLabel(line))
info_row.addStretch()
self.viewLayout.addLayout(info_row)

# Request
self.viewLayout.addWidget(SubtitleLabel("Request"))
self.request_edit = PlainTextEdit()
self.request_edit.setReadOnly(True)
self.request_edit.setMinimumHeight(180)
request_text = json.dumps(self.log_entry.get("request", {}), indent=2, ensure_ascii=False)
request_text = json.dumps(
self.log_entry.get("request", {}), indent=2, ensure_ascii=False
)
self.request_edit.setPlainText(request_text)
self.viewLayout.addWidget(self.request_edit)

Expand All @@ -84,7 +98,9 @@ def _setup_ui(self):
self.response_edit = PlainTextEdit()
self.response_edit.setReadOnly(True)
self.response_edit.setMinimumHeight(180)
response_text = json.dumps(self.log_entry.get("response", {}), indent=2, ensure_ascii=False)
response_text = json.dumps(
self.log_entry.get("response", {}), indent=2, ensure_ascii=False
)
self.response_edit.setPlainText(response_text)
self.viewLayout.addWidget(self.response_edit)

Expand All @@ -103,7 +119,9 @@ def _setup_ui(self):
self.widget.setMinimumWidth(700)

def _copy_request(self):
text = json.dumps(self.log_entry.get("request", {}), indent=2, ensure_ascii=False)
text = json.dumps(
self.log_entry.get("request", {}), indent=2, ensure_ascii=False
)
clipboard = QApplication.clipboard()
if clipboard:
clipboard.setText(text)
Expand All @@ -116,7 +134,9 @@ def _copy_request(self):
)

def _copy_response(self):
text = json.dumps(self.log_entry.get("response", {}), indent=2, ensure_ascii=False)
text = json.dumps(
self.log_entry.get("response", {}), indent=2, ensure_ascii=False
)
clipboard = QApplication.clipboard()
if clipboard:
clipboard.setText(text)
Expand All @@ -135,6 +155,7 @@ class LLMLogsInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("llmLogsInterface")
self.setWindowTitle(self.tr("LLM 请求日志"))

self.all_logs: List[Dict[str, Any]] = []
self.filtered_logs: List[Dict[str, Any]] = []
Expand All @@ -143,6 +164,7 @@ def __init__(self, parent=None):
self._setup_ui()
self._connect_signals()
self._load_logs()
self._setup_file_watcher()

def _setup_ui(self):
self.main_layout = QVBoxLayout(self)
Expand Down Expand Up @@ -213,22 +235,26 @@ def _setup_table(self):
self.table.setBorderVisible(True)
self.table.setBorderRadius(8)

# 减少单元格内边距,让文字显示更多
qss = "QTableView::item { padding-left: 8px; padding-right: 8px; }"
setCustomStyleSheet(self.table, qss, qss)

self.main_layout.addWidget(self.table)

def _setup_footer(self):
"""底部:提示 + 记录数 + 分页"""
"""底部:记录数 + 提示 + 分页"""
footer = QHBoxLayout()
footer.setSpacing(15)

# 左侧:双击提示
hint_label = CaptionLabel(self.tr("双击查看详情"))
hint_label.setStyleSheet("color: gray;")
footer.addWidget(hint_label)

# 记录数
self.status_label = BodyLabel(self.tr("共 0 条"))
footer.addWidget(self.status_label)

# 双击提示
hint_label = CaptionLabel(self.tr("双击查看详情"))
hint_label.setStyleSheet("color: gray;")
footer.addWidget(hint_label)

footer.addStretch()

# 右侧:分页
Expand All @@ -246,13 +272,47 @@ def _setup_footer(self):
self.main_layout.addLayout(footer)

def _connect_signals(self):
self.refresh_btn.clicked.connect(self._load_logs)
self.refresh_btn.clicked.connect(self._on_refresh_clicked)
self.clear_btn.clicked.connect(self._clear_logs)
self.search_edit.textChanged.connect(self._filter_logs)
self.table.doubleClicked.connect(self._show_detail)
self.prev_btn.clicked.connect(self._prev_page)
self.next_btn.clicked.connect(self._next_page)

def _setup_file_watcher(self):
"""设置文件监控,日志文件变化时自动刷新"""
self.file_watcher = QFileSystemWatcher(self)
if LLM_LOG_FILE.exists():
self.file_watcher.addPath(str(LLM_LOG_FILE))
# 同时监控目录,以便检测文件创建
self.file_watcher.addPath(str(LOG_PATH))
self.file_watcher.fileChanged.connect(self._on_file_changed)
self.file_watcher.directoryChanged.connect(self._on_dir_changed)

def _on_file_changed(self, path: str):
"""日志文件内容变化时自动刷新"""
self._load_logs()
# 文件变化后可能需要重新添加监控
if LLM_LOG_FILE.exists() and str(LLM_LOG_FILE) not in self.file_watcher.files():
self.file_watcher.addPath(str(LLM_LOG_FILE))

def _on_dir_changed(self, path: str):
"""目录变化时检查日志文件是否创建"""
if LLM_LOG_FILE.exists() and str(LLM_LOG_FILE) not in self.file_watcher.files():
self.file_watcher.addPath(str(LLM_LOG_FILE))
self._load_logs()

def _on_refresh_clicked(self):
"""手动刷新按钮点击"""
self._load_logs()
InfoBar.success(
title="",
content=self.tr("刷新成功"),
parent=self,
position=InfoBarPosition.TOP,
duration=1000,
)

def _load_logs(self):
"""加载日志文件"""
self.all_logs = []
Expand Down Expand Up @@ -354,7 +414,9 @@ def _update_table(self):
usage = log.get("response", {}).get("usage", {})
total_tokens = usage.get("total_tokens", 0)
if not total_tokens:
total_tokens = usage.get("prompt_tokens", 0) + usage.get("completion_tokens", 0)
total_tokens = usage.get("prompt_tokens", 0) + usage.get(
"completion_tokens", 0
)
self.table.setItem(row, 6, self._create_item(str(total_tokens)))

# 更新分页和统计
Expand Down
14 changes: 12 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ dependencies = [
"requests>=2.32.4",
"openai>=1.97.1",
"diskcache>=5.6.3",
"PyQt5>=5.15.0,<5.16.0",
"PyQt5-Qt5==5.15.2; sys_platform == 'win32'", # Windows 只有此版本
"PyQt5==5.15.11",
"PyQt-Fluent-Widgets==1.8.4",
"yt-dlp>=2025.7.21",
"modelscope>=1.32.0",
Expand Down Expand Up @@ -54,6 +53,17 @@ build-backend = "hatchling.build"
packages = ["app"]

[tool.uv]
# 为不同平台分别解析依赖(PyQt5-Qt5 版本因平台而异)
environments = [
"sys_platform == 'win32'",
"sys_platform == 'darwin'",
"sys_platform == 'linux'",
]
# 覆盖 PyQt5-Qt5 版本:Windows 用 5.15.2,其他平台用最新版
override-dependencies = [
"PyQt5-Qt5==5.15.2; sys_platform == 'win32'",
"PyQt5-Qt5>=5.15.11; sys_platform != 'win32'",
]
dev-dependencies = [
"pyright>=1.1.0",
"ruff>=0.4.0",
Expand Down
Loading
Loading