Skip to content

Commit cc10013

Browse files
authored
Replace poetry with uv & Fix #69 (#70)
* Enhance chatbot model handling for unrecorded models. * Update dev tool from poetry to uv. * Update dev guide in README. * Update default translation models.
1 parent 3e57fbc commit cc10013

16 files changed

+432
-4592
lines changed

.github/workflows/ci.yml

+14-33
Original file line numberDiff line numberDiff line change
@@ -23,42 +23,23 @@ jobs:
2323

2424
runs-on: ${{ matrix.os }}
2525
steps:
26-
- name: Delete huge unnecessary tools folder
27-
run: rm -rf /opt/hostedtoolcache
28-
2926
- uses: actions/checkout@v4
3027

31-
- name: Set up Python ${{ matrix.python-version }}
32-
uses: actions/setup-python@v4
28+
- name: Install the latest version of uv and set the python version
29+
uses: astral-sh/setup-uv@v6
3330
with:
3431
python-version: ${{ matrix.python-version }}
35-
36-
- name: Cache Poetry dependencies
37-
uses: actions/cache@v3
38-
with:
39-
path: |
40-
~/.cache/pypoetry
41-
~/.venv
42-
key: ${{ runner.os }}-poetry-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
43-
restore-keys: |
44-
${{ runner.os }}-poetry-${{ matrix.python-version }}-
45-
${{ runner.os }}-poetry-
46-
${{ runner.os }}-
47-
48-
- name: Install Poetry
49-
uses: snok/install-poetry@v1
50-
with:
51-
version: 1.8.4
52-
- name: Install dependencies
53-
run: |
54-
poetry lock --no-update
55-
poetry install
56-
poetry run pip uninstall -y faster-whisper
57-
poetry run pip install "faster-whisper @ https://github.com/SYSTRAN/faster-whisper/archive/8327d8cc647266ed66f6cd878cf97eccface7351.tar.gz"
58-
- name: Install extra dependencies
59-
run: |
60-
poetry run pip install --force-reinstall torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
61-
poetry run pip install -U typing-extensions
32+
activate-environment: true
33+
# - name: Install dependencies
34+
# run: |
35+
# poetry lock --no-update
36+
# poetry install
37+
# poetry run pip uninstall -y faster-whisper
38+
# poetry run pip install "faster-whisper @ https://github.com/SYSTRAN/faster-whisper/archive/8327d8cc647266ed66f6cd878cf97eccface7351.tar.gz"
39+
# - name: Install extra dependencies
40+
# run: |
41+
# poetry run pip install --force-reinstall torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
42+
# poetry run pip install -U typing-extensions
6243

6344
- name: Install ffmpeg
6445
uses: FedericoCarboni/setup-ffmpeg@v2
@@ -76,4 +57,4 @@ jobs:
7657
- name: Test with unittest
7758
working-directory: ./tests
7859
run: |
79-
poetry run python -m unittest discover -s . -p 'test_*.py'
60+
uv run python -m unittest discover -s . -p 'test_*.py'

README.md

+24
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,30 @@ To maintain context between translation segments, the process is sequential for
263263

264264
[//]: # (## Comparison to https://microsoft.github.io/autogen/docs/notebooks/agentchat_video_transcript_translate_with_whisper/)
265265

266+
## Development Guide
267+
268+
I'm using [uv](https://github.com/astral-sh/uv) for package management.
269+
Install uv with our standalone installers:
270+
271+
#### On macOS and Linux.
272+
273+
```shell
274+
curl -LsSf https://astral.sh/uv/install.sh | sh
275+
```
276+
277+
#### On Windows.
278+
279+
```shell
280+
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
281+
```
282+
283+
### Install deps
284+
285+
```shell
286+
uv venv
287+
uv sync
288+
```
289+
266290
## Todo
267291

268292
- [x] [Efficiency] Batched translate/polish for GPT request (enable contextual ability).

openlrc/agents.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def _initialize_chatbot(self, chatbot_model: Union[str, ModelConfig], fee_limit:
4343
if isinstance(chatbot_model, str):
4444
chatbot_cls: Union[Type[ClaudeBot], Type[GPTBot], Type[GeminiBot]]
4545
chatbot_cls, model_name = route_chatbot(chatbot_model)
46-
return chatbot_cls(model_name=model_name, fee_limit=fee_limit, proxy=proxy, retry=2,
46+
return chatbot_cls(model_name=model_name, fee_limit=fee_limit, proxy=proxy, retry=4,
4747
temperature=self.TEMPERATURE, base_url_config=base_url_config)
4848
elif isinstance(chatbot_model, ModelConfig):
4949
chatbot_cls = provider2chatbot[chatbot_model.provider]
@@ -58,9 +58,11 @@ def _initialize_chatbot(self, chatbot_model: Union[str, ModelConfig], fee_limit:
5858
base_url_config = None
5959
logger.warning(f'Unsupported base_url configuration for provider: {chatbot_model.provider}')
6060

61-
return chatbot_cls(model_name=chatbot_model.name, fee_limit=fee_limit, proxy=proxy, retry=2,
61+
return chatbot_cls(model_name=chatbot_model.name, fee_limit=fee_limit, proxy=proxy, retry=4,
6262
temperature=self.TEMPERATURE, base_url_config=base_url_config,
6363
api_key=chatbot_model.api_key)
64+
else:
65+
raise ValueError(f'Invalid chatbot model type: {type(chatbot_model)}. Expected str or ModelConfig.')
6466

6567

6668
class ChunkedTranslatorAgent(Agent):
@@ -76,7 +78,7 @@ class ChunkedTranslatorAgent(Agent):
7678
TEMPERATURE = 1.0
7779

7880
def __init__(self, src_lang, target_lang, info: TranslateInfo = TranslateInfo(),
79-
chatbot_model: Union[str, ModelConfig] = 'gpt-4o-mini', fee_limit: float = 0.8, proxy: str = None,
81+
chatbot_model: Union[str, ModelConfig] = 'gpt-4.1-nano', fee_limit: float = 0.8, proxy: str = None,
8082
base_url_config: Optional[dict] = None):
8183
"""
8284
Initialize the ChunkedTranslatorAgent.
@@ -190,7 +192,8 @@ def translate_chunk(self, chunk_id: int, chunk: List[Tuple[int, str]],
190192
guideline = context.guideline if use_glossary else context.non_glossary_guideline
191193
messages_list = [
192194
{'role': 'system', 'content': self.prompter.system()},
193-
{'role': 'user', 'content': self.prompter.user(chunk_id, user_input, context.summary, guideline)},
195+
{'role': 'user',
196+
'content': self.prompter.user(chunk_id, user_input, context.previous_summaries, guideline)},
194197
]
195198
resp = self.chatbot.message(messages_list, output_checker=self.prompter.check_format)[0]
196199
translations, summary, scene = self._parse_responses(resp)
@@ -213,7 +216,7 @@ class ContextReviewerAgent(Agent):
213216
TEMPERATURE = 0.6
214217

215218
def __init__(self, src_lang, target_lang, info: TranslateInfo = TranslateInfo(),
216-
chatbot_model: Union[str, ModelConfig] = 'gpt-4o-mini',
219+
chatbot_model: Union[str, ModelConfig] = 'gpt-4.1-nano',
217220
retry_model: Optional[Union[str, ModelConfig]] = None, fee_limit: float = 0.8, proxy: str = None,
218221
base_url_config: Optional[dict] = None):
219222
"""
@@ -359,7 +362,7 @@ class ProofreaderAgent(Agent):
359362
TEMPERATURE = 0.8
360363

361364
def __init__(self, src_lang, target_lang, info: TranslateInfo = TranslateInfo(),
362-
chatbot_model: Union[str, ModelConfig] = 'gpt-4o-mini', fee_limit: float = 0.8, proxy: str = None,
365+
chatbot_model: Union[str, ModelConfig] = 'gpt-4.1-nano', fee_limit: float = 0.8, proxy: str = None,
363366
base_url_config: Optional[dict] = None):
364367
"""
365368
Initialize the ProofreaderAgent.
@@ -432,7 +435,7 @@ class TranslationEvaluatorAgent(Agent):
432435

433436
TEMPERATURE = 0.95
434437

435-
def __init__(self, chatbot_model: Union[str, ModelConfig] = 'gpt-4o-mini', fee_limit: float = 0.8,
438+
def __init__(self, chatbot_model: Union[str, ModelConfig] = 'gpt-4.1-nano', fee_limit: float = 0.8,
436439
proxy: str = None,
437440
base_url_config: Optional[dict] = None):
438441
"""

openlrc/chatbot.py

+73-43
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# All rights reserved.
33

44
import asyncio
5+
import json
56
import os
67
import random
78
import re
@@ -10,15 +11,14 @@
1011
from typing import List, Union, Dict, Callable, Optional
1112

1213
import anthropic
13-
import google.generativeai as genai
1414
import httpx
1515
import openai
1616
from anthropic import AsyncAnthropic
1717
from anthropic._types import NOT_GIVEN
1818
from anthropic.types import Message
19-
from google.generativeai import GenerationConfig
20-
from google.generativeai.types import AsyncGenerateContentResponse, GenerateContentResponse, HarmCategory, \
21-
HarmBlockThreshold
19+
from google import genai
20+
from google.genai import types
21+
from google.genai.types import HarmCategory, HarmBlockThreshold
2222
from openai import AsyncClient as AsyncGPTClient
2323
from openai.types.chat import ChatCompletion
2424

@@ -57,10 +57,7 @@ def route_chatbot(model: str) -> (type, str):
5757
chatbot_type, chatbot_model = re.match(r'(.+):(.+)', model).groups()
5858
chatbot_type, chatbot_model = chatbot_type.strip().lower(), chatbot_model.strip()
5959

60-
try:
61-
Models.get_model(chatbot_model)
62-
except ValueError:
63-
raise ValueError(f'Invalid model {chatbot_model}.')
60+
Models.get_model(chatbot_model)
6461

6562
if chatbot_type == 'openai':
6663
return GPTBot, chatbot_model
@@ -174,7 +171,7 @@ def __str__(self):
174171

175172
@_register_chatbot
176173
class GPTBot(ChatBot):
177-
def __init__(self, model_name='gpt-4o-mini', temperature=1, top_p=1, retry=8, max_async=16, json_mode=False,
174+
def __init__(self, model_name='gpt-4.1-nano', temperature=1, top_p=1, retry=8, max_async=16, json_mode=False,
178175
fee_limit=0.05, proxy=None, base_url_config=None, api_key=None):
179176

180177
# clamp temperature to 0-2
@@ -235,7 +232,8 @@ async def _create_achat(self, messages: List[Dict], stop_sequences: Optional[Lis
235232
continue
236233

237234
break
238-
except (openai.RateLimitError, openai.APITimeoutError, openai.APIConnectionError, openai.APIError) as e:
235+
except (openai.RateLimitError, openai.APITimeoutError, openai.APIConnectionError, openai.APIError,
236+
json.decoder.JSONDecodeError) as e:
239237
sleep_time = self._get_sleep_time(e)
240238
logger.warning(f'{type(e).__name__}: {e}. Wait {sleep_time}s before retry. Retry num: {i + 1}.')
241239
time.sleep(sleep_time)
@@ -251,6 +249,8 @@ def _get_sleep_time(error):
251249
return random.randint(30, 60)
252250
elif isinstance(error, openai.APITimeoutError):
253251
return 3
252+
elif isinstance(error, json.decoder.JSONDecodeError):
253+
return 1
254254
else:
255255
return 15
256256

@@ -341,34 +341,54 @@ def _get_sleep_time(self, error):
341341

342342
@_register_chatbot
343343
class GeminiBot(ChatBot):
344-
def __init__(self, model_name='gemini-2.0-flash-exp', temperature=1, top_p=1, retry=8, max_async=16, fee_limit=0.8,
344+
def __init__(self, model_name='gemini-2.5-flash-preview-04-17', temperature=1, top_p=1, retry=8, max_async=16,
345+
fee_limit=0.8,
345346
proxy=None, base_url_config=None, api_key=None):
346347
self.temperature = max(0, min(1, temperature))
347348

348349
super().__init__(model_name, temperature, top_p, retry, max_async, fee_limit)
349350

350351
self.model_name = model_name
351352

352-
genai.configure(api_key=api_key or os.environ['GOOGLE_API_KEY'])
353-
self.config = GenerationConfig(temperature=self.temperature, top_p=self.top_p)
353+
# genai.configure(api_key=api_key or os.environ['GOOGLE_API_KEY'])
354+
self.client = genai.Client(
355+
api_key=api_key or os.environ['GOOGLE_API_KEY']
356+
)
357+
354358
# Should not block any translation-related content.
355-
self.safety_settings = {
356-
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
357-
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
358-
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
359-
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE
360-
}
359+
# self.safety_settings = {
360+
# HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
361+
# HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
362+
# HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
363+
# HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE
364+
# }
365+
self.safety_settings = [
366+
types.SafetySetting(
367+
category=HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold=HarmBlockThreshold.BLOCK_NONE
368+
),
369+
types.SafetySetting(
370+
category=HarmCategory.HARM_CATEGORY_HARASSMENT, threshold=HarmBlockThreshold.BLOCK_NONE
371+
),
372+
types.SafetySetting(
373+
category=HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold=HarmBlockThreshold.BLOCK_NONE
374+
),
375+
types.SafetySetting(
376+
category=HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold=HarmBlockThreshold.BLOCK_NONE
377+
)
378+
]
379+
self.config = types.GenerateContentConfig(temperature=self.temperature, top_p=self.top_p,
380+
safety_settings=self.safety_settings)
361381

362382
if proxy:
363383
logger.warning('Google Gemini SDK does not support proxy, try using the system-level proxy if needed.')
364384

365385
if base_url_config:
366386
logger.warning('Google Gemini SDK does not support changing base_url.')
367387

368-
def update_fee(self, response: Union[GenerateContentResponse, AsyncGenerateContentResponse]):
388+
def update_fee(self, response: types.GenerateContentResponse):
369389
model_info = self.model_info
370390
prompt_tokens = response.usage_metadata.prompt_token_count
371-
completion_tokens = response.usage_metadata.candidates_token_count
391+
completion_tokens = response.usage_metadata.candidates_token_count or 0
372392

373393
self.api_fees[-1] += (prompt_tokens * model_info.input_price +
374394
completion_tokens * model_info.output_price) / 1000000
@@ -401,31 +421,41 @@ async def _create_achat(self, messages: List[Dict], stop_sequences: Optional[Lis
401421
history_messages[i]['parts'] = [{'text': content}]
402422

403423
self.config.stop_sequences = stop_sequences
404-
generative_model = genai.GenerativeModel(model_name=self.model_name, generation_config=self.config,
405-
safety_settings=self.safety_settings, system_instruction=system_msg)
406-
client = genai.ChatSession(generative_model, history=history_messages)
424+
# generative_model = genai.GenerativeModel(model_name=self.model_name, generation_config=self.config,
425+
# safety_settings=self.safety_settings, system_instruction=system_msg)
426+
# client = genai.ChatSession(generative_model, history=history_messages)
427+
self.config.system_instruction = system_msg
407428

408429
response = None
409430
for i in range(self.retry):
410-
try:
411-
# send_message_async is buggy, so we use send_message instead as a workaround
412-
response = client.send_message(user_msg, safety_settings=self.safety_settings)
413-
self.update_fee(response)
414-
if not output_checker(user_msg, response.text):
415-
logger.warning(f'Invalid response format. Retry num: {i + 1}.')
416-
continue
417-
418-
if not response._done:
419-
logger.warning(f'Failed to get a complete response. Retry num: {i + 1}.')
420-
continue
421-
422-
break
423-
except (genai.types.BrokenResponseError, genai.types.IncompleteIterationError,
424-
genai.types.StopCandidateException) as e:
425-
logger.warning(f'{type(e).__name__}: {e}. Retry num: {i + 1}.')
426-
except genai.types.generation_types.BlockedPromptException as e:
427-
logger.warning(f'Prompt blocked: {e}.\n Retry in 30s.')
428-
time.sleep(30)
431+
# try:
432+
# send_message_async is buggy, so we use send_message instead as a workaround
433+
# response = client.send_message(user_msg, safety_settings=self.safety_settings)
434+
response = await self.client.aio.models.generate_content(
435+
model=self.model_name,
436+
contents=user_msg,
437+
config=self.config,
438+
)
439+
self.update_fee(response)
440+
if not response.text:
441+
logger.warning(f'Get None response. Wait 15s. Retry num: {i + 1}.')
442+
time.sleep(15)
443+
continue
444+
445+
if not output_checker(user_msg, response.text):
446+
logger.warning(f'Invalid response format. Retry num: {i + 1}.')
447+
continue
448+
449+
if not response:
450+
logger.warning(f'Failed to get a complete response. Retry num: {i + 1}.')
451+
continue
452+
453+
break
454+
# except Exception as e:
455+
# logger.warning(f'{type(e).__name__}: {e}. Retry num: {i + 1}.')
456+
# time.sleep(3)
457+
# except genai.types.generation_types.BlockedPromptException as e:
458+
# logger.warning(f'Prompt blocked: {e}.\n Retry in 30s.')
429459

430460
if not response:
431461
raise ChatBotException('Failed to create a chat.')

openlrc/context.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
# Copyright (C) 2024. Hao Zheng
1+
# Copyright (C) 2025. Hao Zheng
22
# All rights reserved.
33
import re
4-
from typing import Optional, Union
4+
from typing import Optional, Union, List
55

66
from pydantic import BaseModel
77

88
from openlrc import ModelConfig
99

1010

1111
class TranslationContext(BaseModel):
12+
previous_summaries: Optional[List[str]] = None
1213
summary: Optional[str] = ''
1314
scene: Optional[str] = ''
1415
model: Optional[Union[str, ModelConfig]] = None

openlrc/evaluate.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2024. Hao Zheng
1+
# Copyright (C) 2025. Hao Zheng
22
# All rights reserved.
33
import abc
44
from typing import Union
@@ -26,7 +26,7 @@ class LLMTranslationEvaluator(TranslationEvaluator):
2626
Evaluate the translated texts using large language models.
2727
"""
2828

29-
def __init__(self, chatbot_model: Union[str, ModelConfig] = 'gpt-4o-mini'):
29+
def __init__(self, chatbot_model: Union[str, ModelConfig] = 'gpt-4.1-nano'):
3030
self.agenet = TranslationEvaluatorAgent(chatbot_model=chatbot_model)
3131

3232
def evaluate(self, src_texts, target_texts, src_lang=None, target_lang=None):

0 commit comments

Comments
 (0)