Initial commit
This commit is contained in:
parent
d38a6dd66b
commit
be9a605dac
28 changed files with 1994 additions and 0 deletions
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
11
.idea/Srtify.iml
generated
Normal file
11
.idea/Srtify.iml
generated
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="uv (Srtify)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
7
.idea/dictionaries/project.xml
generated
Normal file
7
.idea/dictionaries/project.xml
generated
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="project">
|
||||||
|
<words>
|
||||||
|
<w>srtify</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
12
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
12
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredIdentifiers">
|
||||||
|
<list>
|
||||||
|
<option value="os.*" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="uv (Srtify)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="uv (Srtify)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/Srtify.iml" filepath="$PROJECT_DIR$/.idea/Srtify.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
1
.python-version
Normal file
1
.python-version
Normal file
|
@ -0,0 +1 @@
|
||||||
|
3.13
|
6
main.py
Normal file
6
main.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
def main():
|
||||||
|
print("Hello from srtify!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
26
pyproject.toml
Normal file
26
pyproject.toml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=65.5.1", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "srtify"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "CLI tool for translating SRT files using Gemini AI"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"chardet>=5.2.0",
|
||||||
|
"click>=8.2.1",
|
||||||
|
"gemini-srt-translator>=2.1.3",
|
||||||
|
"platformdirs>=4.3.8",
|
||||||
|
"setuptools>=65.5.1",
|
||||||
|
"simple-term-menu>=1.6.6",
|
||||||
|
"wheel>=0.45.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
srtify = "srtify.__main__:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
include = ["srtify*"]
|
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
0
src/srtify/__init__.py
Normal file
0
src/srtify/__init__.py
Normal file
18
src/srtify/__main__.py
Normal file
18
src/srtify/__main__.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import sys
|
||||||
|
from srtify.cli.commands import cli
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
""" Main entry point for the application. """
|
||||||
|
try:
|
||||||
|
cli()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nGoodbye!")
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ An error occurred: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
0
src/srtify/cli/__init__.py
Normal file
0
src/srtify/cli/__init__.py
Normal file
276
src/srtify/cli/cli_handler.py
Normal file
276
src/srtify/cli/cli_handler.py
Normal file
|
@ -0,0 +1,276 @@
|
||||||
|
import shutil
|
||||||
|
from .menu import MainMenu, PromptMenu, SettingsMenu
|
||||||
|
from srtify.core.prompts import PromptsManager
|
||||||
|
from srtify.core.settings import SettingsManager
|
||||||
|
from srtify.utils.utils import clear_screen, fancy_headline
|
||||||
|
|
||||||
|
|
||||||
|
class CliHandler:
|
||||||
|
def __init__(self, settings_manager: SettingsManager, prompts_manager: PromptsManager):
|
||||||
|
self.settings_manager = settings_manager
|
||||||
|
self.prompts_manager = prompts_manager
|
||||||
|
self.settings_menu = SettingsMenu()
|
||||||
|
|
||||||
|
def handle_main_menu(self):
|
||||||
|
"""Handle the main menu."""
|
||||||
|
main_menu = MainMenu()
|
||||||
|
|
||||||
|
|
||||||
|
while True:
|
||||||
|
clear_screen()
|
||||||
|
print(fancy_headline("Gemini SRT Translator", "rounded"))
|
||||||
|
summary = self.settings_manager.get_settings_summary()
|
||||||
|
print(f"API Key: {summary['API Key']}")
|
||||||
|
print(f"Target Language: {summary['Language']}")
|
||||||
|
print(f"Input Directory: {summary['Input Directory']}")
|
||||||
|
print(f"Output Directory: {summary['Output Directory']}")
|
||||||
|
print(f"Batch Size: {summary['Batch Size']}")
|
||||||
|
|
||||||
|
choice = main_menu.select()
|
||||||
|
|
||||||
|
if choice is None or choice == "exit":
|
||||||
|
print(fancy_headline("Goodbye!", "rounded"))
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
match choice:
|
||||||
|
case "translate":
|
||||||
|
return "translate"
|
||||||
|
case "prompts":
|
||||||
|
self.handle_prompts_menu()
|
||||||
|
case "settings":
|
||||||
|
self.handle_settings_menu()
|
||||||
|
case "quick":
|
||||||
|
# TODO: Implement quick translation
|
||||||
|
pass
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nReturning to main menu...")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error in Main Menu: {e}")
|
||||||
|
input("Press Enter to continue...")
|
||||||
|
|
||||||
|
def handle_prompts_menu(self):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
print(fancy_headline("Prompts Menu", "rounded"))
|
||||||
|
|
||||||
|
prompts = self.prompts_manager.get_all_prompts()
|
||||||
|
prompts_menu = PromptMenu(prompts)
|
||||||
|
name, choice = prompts_menu.select(clear_screen=True)
|
||||||
|
|
||||||
|
if choice == 'new':
|
||||||
|
self.add_prompt()
|
||||||
|
elif choice == 'main':
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print(f"Selected prompt: {name}: {choice}")
|
||||||
|
self.select_prompt(name, choice)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nReturning to main menu...")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error in Prompts Menu: {e}")
|
||||||
|
input("Press Enter to continue...")
|
||||||
|
|
||||||
|
def add_prompt(self):
|
||||||
|
"""Add a new prompt."""
|
||||||
|
name = input("Enter prompt name: ").strip()
|
||||||
|
if not name:
|
||||||
|
print("❌ Prompt name cannot be empty!")
|
||||||
|
return
|
||||||
|
if self.prompts_manager.prompt_exists(name):
|
||||||
|
print(f"❌ Prompt '{name}' already exists!")
|
||||||
|
input("Press Enter to continue...")
|
||||||
|
return
|
||||||
|
|
||||||
|
description = input("Enter prompt description: ").strip()
|
||||||
|
if not description:
|
||||||
|
print("❌ Prompt description cannot be empty!")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.prompts_manager.add_prompt(name, description):
|
||||||
|
print(f"✓ Prompt '{name}' added successfully!")
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to add prompt '{name}'!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
|
||||||
|
input("Press Enter to continue...")
|
||||||
|
|
||||||
|
def select_prompt(self, *prompt):
|
||||||
|
"""Select a prompt."""
|
||||||
|
prompt_menu = PromptMenu()
|
||||||
|
choice = prompt_menu.prompt_options()
|
||||||
|
|
||||||
|
if choice is None:
|
||||||
|
print("❌ No option selected!")
|
||||||
|
return
|
||||||
|
|
||||||
|
if choice == "select":
|
||||||
|
pass
|
||||||
|
elif choice == "edit":
|
||||||
|
self.edit_prompt(*prompt)
|
||||||
|
elif choice == "delete":
|
||||||
|
self.delete_prompt(*prompt)
|
||||||
|
elif choice == "back":
|
||||||
|
return
|
||||||
|
|
||||||
|
def edit_prompt(self, *prompt):
|
||||||
|
"""Edit a prompt."""
|
||||||
|
print(f"Prompt: {prompt[0]}")
|
||||||
|
print('-' * (len(prompt[0]) + 8))
|
||||||
|
print(f"Description: {prompt[1]}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("=" * shutil.get_terminal_size().columns)
|
||||||
|
new_prompt = input("Enter new description: ").strip()
|
||||||
|
if new_prompt and self.prompts_manager.update_prompt(prompt[0], new_prompt):
|
||||||
|
print(f"✓ Prompt '{prompt[0]}' updated successfully!")
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to update prompt '{prompt[0]}'!")
|
||||||
|
except (ValueError, IndexError) as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
|
||||||
|
def delete_prompt(self, *prompt):
|
||||||
|
"""Delete a prompt."""
|
||||||
|
try:
|
||||||
|
if input(f"Are you sure you want to delete prompt '{prompt[0]}'? (y/n) ").strip().lower().endswith('y'):
|
||||||
|
if self.prompts_manager.delete_prompt(prompt[0]):
|
||||||
|
print(f"✓ Prompt '{prompt[0]}' deleted successfully!")
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to delete prompt '{prompt[0]}'!")
|
||||||
|
except (ValueError, IndexError) as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
|
||||||
|
def search_prompts(self, search_term):
|
||||||
|
"""Search for prompts."""
|
||||||
|
if not search_term:
|
||||||
|
search_term = input("Enter search term: ").strip()
|
||||||
|
if not search_term:
|
||||||
|
print("❌ Search term cannot be empty!")
|
||||||
|
exit()
|
||||||
|
|
||||||
|
results = self.prompts_manager.search_prompts(search_term)
|
||||||
|
if results:
|
||||||
|
if len(results) > 1:
|
||||||
|
print(f"Found {len(results)} prompts matching '{search_term}':")
|
||||||
|
print("=" * shutil.get_terminal_size().columns)
|
||||||
|
for i, name, description in enumerate(results.items()):
|
||||||
|
print(f"{i+1} - {name}: {description}")
|
||||||
|
print("=" * shutil.get_terminal_size().columns)
|
||||||
|
choice = input("Enter the number of the prompt you want to select: ").strip()
|
||||||
|
if choice.isdigit() and 1 <= int(choice) <= len(results):
|
||||||
|
prompt_name = list(results.keys())[int(choice) - 1]
|
||||||
|
print(f"Selected prompt: {prompt_name}")
|
||||||
|
return results[prompt_name]
|
||||||
|
else:
|
||||||
|
print("❌ Invalid choice!")
|
||||||
|
exit()
|
||||||
|
else:
|
||||||
|
for name, description in results.items():
|
||||||
|
print(f"Found prompt '{name}':\n{description}")
|
||||||
|
return results[name]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def handle_settings_menu(self):
|
||||||
|
"""Handle the settings menu."""
|
||||||
|
while True:
|
||||||
|
summary = self.settings_manager.get_settings_summary()
|
||||||
|
choice = self.settings_menu.select()
|
||||||
|
|
||||||
|
if choice is None:
|
||||||
|
print("Settings selection cancelled.")
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
match choice:
|
||||||
|
case 'view':
|
||||||
|
print(fancy_headline("Settings Summary"))
|
||||||
|
for key, value in summary.items():
|
||||||
|
print(f" {key}: {value}")
|
||||||
|
print("=" * 50)
|
||||||
|
input("Press Enter to continue...")
|
||||||
|
case 'api':
|
||||||
|
self.set_api_key()
|
||||||
|
case 'dirs':
|
||||||
|
self.set_directories()
|
||||||
|
case 'translation':
|
||||||
|
self.set_translation_preferences()
|
||||||
|
case 'reset':
|
||||||
|
if input("Reset all settings to default? (y/N) ").strip().lower().endswith('y'):
|
||||||
|
print("Resetting settings...")
|
||||||
|
if self.settings_manager.reset_to_defaults():
|
||||||
|
print("✓ Settings reset to default successfully!")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to reset settings to default!")
|
||||||
|
else:
|
||||||
|
print("Settings reset cancelled.")
|
||||||
|
input("Press Enter to continue...")
|
||||||
|
case 'back':
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nReturning to main menu...")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error in Settings Menu: {e}")
|
||||||
|
input("Press Enter to continue...")
|
||||||
|
|
||||||
|
def set_api_key(self):
|
||||||
|
"""Set API key."""
|
||||||
|
current_key = self.settings_manager.get_api_key()
|
||||||
|
if current_key:
|
||||||
|
masked_key = f"...{current_key[-6:]}" if len(current_key) > 6 else "Set"
|
||||||
|
print(f"Current API Key: {masked_key}")
|
||||||
|
|
||||||
|
new_key = input("Enter new API key: ").strip()
|
||||||
|
if new_key:
|
||||||
|
try:
|
||||||
|
if self.settings_manager.set_api_key(new_key):
|
||||||
|
print("✓ API key set successfully!")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to set API key!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"❌ Error setting API key: {e}")
|
||||||
|
|
||||||
|
def set_directories(self):
|
||||||
|
"""Set input and output directories."""
|
||||||
|
current_input, current_output = self.settings_manager.get_directories()
|
||||||
|
print(f"Current Input Directory: {current_input}")
|
||||||
|
print(f"Current Output Directory: {current_output}")
|
||||||
|
|
||||||
|
new_input = input("Enter new input directory: ").strip()
|
||||||
|
new_output = input("Enter new output directory: ").strip()
|
||||||
|
|
||||||
|
if new_input or new_output:
|
||||||
|
try:
|
||||||
|
if self.settings_manager.set_directories(
|
||||||
|
new_input or current_input,
|
||||||
|
new_output or current_output
|
||||||
|
):
|
||||||
|
print("✓ Directories set successfully!")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to set directories!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"❌ Error setting directories: {e}")
|
||||||
|
|
||||||
|
def set_translation_preferences(self):
|
||||||
|
"""Set translation preferences."""
|
||||||
|
current_lang, current_batch = self.settings_manager.get_translation_config()
|
||||||
|
|
||||||
|
print(f"Current Language: {current_lang}")
|
||||||
|
print(f"Current Batch Size: {current_batch}")
|
||||||
|
|
||||||
|
new_lang = input("Enter new language (or leave blank to keep current): ").strip()
|
||||||
|
new_batch = input("Enter new batch size (or leave blank to keep current): ").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
batch_size = int(new_batch) if new_batch else current_batch
|
||||||
|
language = new_lang if new_lang else current_lang
|
||||||
|
|
||||||
|
if self.settings_manager.set_translation_config(language, batch_size):
|
||||||
|
print("✓ Translation preferences set successfully!")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to set translation preferences!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"❌ Error setting translation preferences: {e}")
|
104
src/srtify/cli/commands.py
Normal file
104
src/srtify/cli/commands.py
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import click
|
||||||
|
from pathlib import Path
|
||||||
|
from .cli_handler import CliHandler
|
||||||
|
from srtify.core.translator import TranslatorApp
|
||||||
|
from srtify.core.settings import SettingsManager
|
||||||
|
from srtify.core.prompts import PromptsManager
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(invoke_without_command=True)
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx):
|
||||||
|
""" Gemini SRT Translator - Translate subtitle files using AI """
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
settings = SettingsManager()
|
||||||
|
prompts = PromptsManager()
|
||||||
|
cli_handler = CliHandler(settings, prompts)
|
||||||
|
cli_handler.handle_main_menu()
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.argument('input_path', type=click.Path(exists=True, path_type=Path))
|
||||||
|
@click.option('--output', '-o', type=click.Path(path_type=Path),
|
||||||
|
help='Output directory (default: from settings)')
|
||||||
|
@click.option('--language', '-l', help='Target language (default: from settings)')
|
||||||
|
@click.option('--file', '-f', help='Specific file to translate (default: all .srt files)')
|
||||||
|
@click.option('--prompt', '-p', help='Search for a specific prompt by name')
|
||||||
|
@click.option('--custom-prompt', help='Use a custom prompt')
|
||||||
|
@click.option('--batch-size', '-b', type=int, help='Batch size for translation')
|
||||||
|
@click.option('-quick', '-q', is_flag=True, help='Quick translation with default prompt')
|
||||||
|
def translate(input_path, output, language, file, prompt, custom_prompt, batch_size, quick):
|
||||||
|
""" Translate SRT files from INPUT_PATH """
|
||||||
|
settings = SettingsManager()
|
||||||
|
prompts = PromptsManager()
|
||||||
|
|
||||||
|
app = TranslatorApp(settings, prompts)
|
||||||
|
|
||||||
|
options = {
|
||||||
|
'input_path': input_path,
|
||||||
|
'output_path': output,
|
||||||
|
'language': language,
|
||||||
|
'file': file,
|
||||||
|
'prompt': prompt,
|
||||||
|
'custom_prompt': custom_prompt,
|
||||||
|
'batch_size': batch_size,
|
||||||
|
'quick': quick
|
||||||
|
}
|
||||||
|
|
||||||
|
app.run_translation(options)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def interactive():
|
||||||
|
""" Start interactive translation mode ."""
|
||||||
|
settings = SettingsManager()
|
||||||
|
prompts = PromptsManager()
|
||||||
|
cli_handler = CliHandler(settings, prompts)
|
||||||
|
cli_handler.handle_main_menu()
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def settings():
|
||||||
|
""" Configure application settings. """
|
||||||
|
settings = SettingsManager()
|
||||||
|
prompts = PromptsManager()
|
||||||
|
cli_handler = CliHandler(settings, prompts)
|
||||||
|
cli_handler.handle_settings_menu()
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def prompts():
|
||||||
|
""" Manage translation prompts. """
|
||||||
|
settings = SettingsManager()
|
||||||
|
prompts = PromptsManager()
|
||||||
|
cli_handler = CliHandler(settings, prompts)
|
||||||
|
cli_handler.handle_prompts_menu()
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument('search_term')
|
||||||
|
def search_prompts(search_term):
|
||||||
|
""" Search for prompts by name or description. """
|
||||||
|
prompts = PromptsManager()
|
||||||
|
results = prompts.search_prompts(search_term)
|
||||||
|
|
||||||
|
if results:
|
||||||
|
click.echo(f"Found {len(results)} prompts matching {search_term}...")
|
||||||
|
for name, description in results.items():
|
||||||
|
click.echo(f" {name}: {description[:100]}{'...' if len(description) else ''}")
|
||||||
|
else:
|
||||||
|
click.echo(f"No prompts found matching '{search_term}'")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def status():
|
||||||
|
""" Show current configuration status. """
|
||||||
|
settings = SettingsManager()
|
||||||
|
prompts = PromptsManager()
|
||||||
|
|
||||||
|
click.echo("=== Configuration Status ===")
|
||||||
|
summary = settings.get_settings_summary()
|
||||||
|
for key, value in summary.items():
|
||||||
|
click.echo(f" {key}: {value}")
|
||||||
|
|
||||||
|
click.echo(f"\nPrompts: {prompts.count_prompts()} total")
|
110
src/srtify/cli/menu.py
Normal file
110
src/srtify/cli/menu.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
from typing import Dict
|
||||||
|
from simple_term_menu import TerminalMenu
|
||||||
|
|
||||||
|
|
||||||
|
class Menu:
|
||||||
|
def __init__(self, title, style=None):
|
||||||
|
self.title = title
|
||||||
|
self.style = style or {
|
||||||
|
"menu_highlight_style": ("bg_green", "fg_black"),
|
||||||
|
"search_highlight_style": ("bg_red", "fg_black"),
|
||||||
|
"cycle_cursor": True,
|
||||||
|
"search_key": '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
def show(self, items, clear_screen=False):
|
||||||
|
return TerminalMenu(
|
||||||
|
items,
|
||||||
|
clear_screen=clear_screen,
|
||||||
|
title=self.title,
|
||||||
|
**self.style
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MainMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("====== Gemini SRT Translator ======")
|
||||||
|
self.options = {
|
||||||
|
"Start Translation": "translate",
|
||||||
|
"Prompts Menu": "prompts",
|
||||||
|
"Settings Menu": "settings",
|
||||||
|
"Quick Translation (Default Settings)": "quick",
|
||||||
|
"Exit": "exit"
|
||||||
|
}
|
||||||
|
|
||||||
|
def select(self):
|
||||||
|
option_names = list(self.options.keys())
|
||||||
|
menu = self.show(option_names, clear_screen=False)
|
||||||
|
selected_index = menu.show()
|
||||||
|
|
||||||
|
if selected_index is not None:
|
||||||
|
selected_key = option_names[selected_index]
|
||||||
|
return self.options[selected_key]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class PromptMenu(Menu):
|
||||||
|
def __init__(self, prompts_list: Dict[str, str] = None):
|
||||||
|
super().__init__("====== Prompt Menu ======")
|
||||||
|
self.prompts_list = prompts_list if prompts_list is not None else {}
|
||||||
|
self.options = {
|
||||||
|
"Add New Prompt": "new",
|
||||||
|
"Back to Main Menu": "main"
|
||||||
|
}
|
||||||
|
# if prompts_list:
|
||||||
|
self.prompts_list.update(self.options)
|
||||||
|
|
||||||
|
def select(self, clear_screen=False):
|
||||||
|
prompts_list = list(self.prompts_list.keys())
|
||||||
|
menu = self.show(prompts_list, clear_screen)
|
||||||
|
selected_index = menu.show()
|
||||||
|
|
||||||
|
if selected_index is not None:
|
||||||
|
selected_key = prompts_list[selected_index]
|
||||||
|
selected_value = self.prompts_list[selected_key]
|
||||||
|
return selected_key, selected_value
|
||||||
|
|
||||||
|
print("No option selected.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def prompt_options(self):
|
||||||
|
options = {
|
||||||
|
"Select": "select",
|
||||||
|
"Edit": "edit",
|
||||||
|
"Delete": "delete",
|
||||||
|
"Back": "back"
|
||||||
|
}
|
||||||
|
options_list = list(options.keys())
|
||||||
|
menu = self.show(options_list, clear_screen=True)
|
||||||
|
selected_index = menu.show()
|
||||||
|
|
||||||
|
if selected_index is not None:
|
||||||
|
selected_key = options_list[selected_index]
|
||||||
|
return options[selected_key]
|
||||||
|
|
||||||
|
print("No option selected.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("====== Settings Menu ======")
|
||||||
|
self.options = {
|
||||||
|
"View Current Settings": "view",
|
||||||
|
"Set API Key": "api",
|
||||||
|
"Set Input/Output Directories": "dirs",
|
||||||
|
"Set Translation Preferences": "translation",
|
||||||
|
"Reset to Default": "reset",
|
||||||
|
"Back to Main Menu": "back"
|
||||||
|
}
|
||||||
|
|
||||||
|
def select(self):
|
||||||
|
option_names = list(self.options.keys())
|
||||||
|
menu = self.show(option_names, clear_screen=True)
|
||||||
|
selected_index = menu.show()
|
||||||
|
|
||||||
|
if selected_index is not None:
|
||||||
|
return self.options[option_names[selected_index]]
|
||||||
|
print("Settings selection cancelled.")
|
||||||
|
return None
|
0
src/srtify/core/__init__.py
Normal file
0
src/srtify/core/__init__.py
Normal file
62
src/srtify/core/file_encoding.py
Normal file
62
src/srtify/core/file_encoding.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
import chardet
|
||||||
|
|
||||||
|
|
||||||
|
def detect_and_fix_encoding(path: str, file_name: str, target_encoding='utf-8'):
|
||||||
|
"""
|
||||||
|
Detect and convert file encoding to target encoding, replacing the original file.
|
||||||
|
:param path:
|
||||||
|
:param file_name:
|
||||||
|
:param target_encoding:
|
||||||
|
:return: bool: True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
|
||||||
|
path = Path(path)
|
||||||
|
file_path = path / file_name
|
||||||
|
if not file_path.exists():
|
||||||
|
print(f"✗ File {file_path} does not exist.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
raw_data = f.read()
|
||||||
|
encoding_info = chardet.detect(raw_data)
|
||||||
|
detected_encoding = encoding_info['encoding']
|
||||||
|
|
||||||
|
if not detected_encoding:
|
||||||
|
print(f"✗ Unable to detect encoding for file {file_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
accepted_encodings = ['utf-8', 'utf-8-sig']
|
||||||
|
if detected_encoding.lower() in accepted_encodings:
|
||||||
|
print(f"✓ Encoding for file {file_name} is already {detected_encoding}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
print(f"Converting {file_name} from {detected_encoding} to {target_encoding}...")
|
||||||
|
|
||||||
|
with open(file_path, 'r', encoding=detected_encoding) as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', encoding=target_encoding,
|
||||||
|
dir=path, delete=False) as temp_file:
|
||||||
|
temp_file.write(content)
|
||||||
|
temp_path = Path(temp_file.name)
|
||||||
|
|
||||||
|
temp_path.replace(file_path)
|
||||||
|
|
||||||
|
print(f"✓ Converted {file_name} from {detected_encoding} to {target_encoding}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except (UnicodeDecodeError, UnicodeEncodeError) as e:
|
||||||
|
print(f"✗ Error converting {file_name} to {target_encoding}: {e}")
|
||||||
|
if 'temp_path' in locals() and os.path.exists(temp_path):
|
||||||
|
temp_path.unlink()
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error processing {file_name}: {e}")
|
||||||
|
if 'temp_path' in locals() and os.path.exists(temp_path):
|
||||||
|
temp_path.unlink()
|
||||||
|
return False
|
166
src/srtify/core/models.py
Normal file
166
src/srtify/core/models.py
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from typing import Dict, Optional, Any
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ApiConfig:
|
||||||
|
"""API Configuration settings."""
|
||||||
|
gemini_key: str = ""
|
||||||
|
|
||||||
|
def is_valid(self) -> bool:
|
||||||
|
return bool(self.gemini_key.strip())
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DirectoryConfig:
|
||||||
|
"""Directory configuration settings."""
|
||||||
|
input_dir: str = ""
|
||||||
|
output_dir: str = ""
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
"""Expand user paths after initialization."""
|
||||||
|
if self.input_dir:
|
||||||
|
self.input_dir = os.path.expanduser(self.input_dir)
|
||||||
|
if self.output_dir:
|
||||||
|
self.output_dir = os.path.expanduser(self.output_dir)
|
||||||
|
|
||||||
|
def validate(self) -> bool:
|
||||||
|
"""Validate directory paths."""
|
||||||
|
if not self.input_dir or not self.output_dir:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
Path(self.input_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
Path(self.output_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
return True
|
||||||
|
except (OSError, PermissionError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TranslationConfig:
|
||||||
|
"""Translation configuration settings."""
|
||||||
|
default_language: str = "persian"
|
||||||
|
batch_size: int = 300
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.batch_size <= 0:
|
||||||
|
self.batch_size = 300
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AppSettings:
|
||||||
|
api: ApiConfig = field(default_factory=ApiConfig)
|
||||||
|
directories: DirectoryConfig = field(default_factory=DirectoryConfig)
|
||||||
|
translation: TranslationConfig = field(default_factory=TranslationConfig)
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Convert settings to dictionary."""
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Dict[str, Any]) -> 'AppSettings':
|
||||||
|
api_data = data.get('api', {})
|
||||||
|
dir_data = data.get('directories', {})
|
||||||
|
trans_data = data.get('translation', {})
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
api=ApiConfig(**api_data),
|
||||||
|
directories=DirectoryConfig(**dir_data),
|
||||||
|
translation=TranslationConfig(**trans_data)
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self) -> bool:
|
||||||
|
return (self.api.is_valid() and
|
||||||
|
self.directories.validate() and
|
||||||
|
self.translation.batch_size > 0)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PromptItem:
|
||||||
|
"""Individual prompt item."""
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
created_at: Optional[str] = None
|
||||||
|
last_used: Optional[str] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
"""Set creation item if not provided."""
|
||||||
|
if not self.created_at:
|
||||||
|
self.created_at = datetime.now().isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PromptsCollection:
|
||||||
|
"""Collection of prompts with metadata."""
|
||||||
|
prompts: Dict[str, PromptItem] = field(default_factory=dict)
|
||||||
|
version: str = "1.0"
|
||||||
|
|
||||||
|
def add_prompt(self, name: str, description: str) -> bool:
|
||||||
|
"""Add a new prompt."""
|
||||||
|
if name in self.prompts:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.prompts[name] = PromptItem(name=name, description=description)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update_prompt(self, name: str, description: str) -> bool:
|
||||||
|
"""Update an existing prompt."""
|
||||||
|
if name not in self.prompts:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.prompts[name].description = description
|
||||||
|
return True
|
||||||
|
|
||||||
|
def delete_prompt(self, name: str) -> bool:
|
||||||
|
"""Delete a prompt."""
|
||||||
|
if name not in self.prompts:
|
||||||
|
return False
|
||||||
|
|
||||||
|
del self.prompts[name]
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_prompt_names(self) -> list[str]:
|
||||||
|
"""Get a list of prompt names."""
|
||||||
|
return list(self.prompts.keys())
|
||||||
|
|
||||||
|
def get_prompt_descriptions(self) -> Dict[str, str]:
|
||||||
|
"""Get dictionary of name -> description mapping."""
|
||||||
|
return {name: prompt.description for name, prompt in self.prompts.items()}
|
||||||
|
|
||||||
|
def mark_used(self, name: str):
|
||||||
|
"""Mark prompt as recently used."""
|
||||||
|
if name in self.prompts:
|
||||||
|
self.prompts[name].last_used = datetime.now().isoformat()
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Convert dictionary from JSON serialization."""
|
||||||
|
return {
|
||||||
|
'prompts': {name: asdict(prompt) for name, prompt in self.prompts.items()},
|
||||||
|
'version': self.version
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Dict[str, Any]) -> 'PromptsCollection':
|
||||||
|
"""Create instance from dictionary."""
|
||||||
|
prompts_data = data.get('prompts', {})
|
||||||
|
prompts = {}
|
||||||
|
|
||||||
|
for name, prompt_data in prompts_data.items():
|
||||||
|
prompts[name] = PromptItem(**prompt_data)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
prompts=prompts,
|
||||||
|
version=data.get('version', '1.0')
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_legacy_dict(cls, data: Dict[str, str]) -> 'PromptsCollection':
|
||||||
|
"""Create instance from legacy dictionary."""
|
||||||
|
collection = cls()
|
||||||
|
for name, description in data.items():
|
||||||
|
collection.add_prompt(name, description)
|
||||||
|
return collection
|
255
src/srtify/core/prompts.py
Normal file
255
src/srtify/core/prompts.py
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from srtify.core.models import PromptsCollection, PromptItem
|
||||||
|
from srtify.utils.paths import get_prompts_path
|
||||||
|
|
||||||
|
|
||||||
|
class PromptsManager:
|
||||||
|
"""Prompts manager class."""
|
||||||
|
def __init__(self, file_path: str = None):
|
||||||
|
self.file_path = Path(file_path) if file_path else get_prompts_path()
|
||||||
|
self.collection = self._load_prompts()
|
||||||
|
|
||||||
|
def _load_prompts(self) -> PromptsCollection:
|
||||||
|
"""Load prompts from file or create an empty collection."""
|
||||||
|
self.file_path.parent.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
if not self.file_path.exists():
|
||||||
|
collection = PromptsCollection()
|
||||||
|
self._save_prompts(collection)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.file_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
if isinstance(data, dict) and 'prompts' not in data:
|
||||||
|
print("Converting legacy prompts format...")
|
||||||
|
collection = PromptsCollection.from_legacy_dict(data)
|
||||||
|
self._save_prompts(collection)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
return PromptsCollection.from_dict(data)
|
||||||
|
|
||||||
|
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
||||||
|
print(f"Error loading prompts file: {e}. Creating new collection.")
|
||||||
|
|
||||||
|
backup_file = self.file_path.with_suffix('.json.backup')
|
||||||
|
if self.file_path.exists():
|
||||||
|
self.file_path.rename(backup_file)
|
||||||
|
print(f"Backed up corrupt prompts file to {backup_file}")
|
||||||
|
|
||||||
|
collection = PromptsCollection()
|
||||||
|
self._save_prompts(collection)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
def _save_prompts(self, collection: Optional[PromptsCollection] = None) -> bool:
|
||||||
|
"""Save current prompts to file."""
|
||||||
|
if collection is None:
|
||||||
|
collection = self.collection
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.file_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(collection.to_dict(), f, indent=2, ensure_ascii=False)
|
||||||
|
return True
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
print(f"Error saving prompts file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def reload(self) -> bool:
|
||||||
|
"""Reload prompts from file."""
|
||||||
|
try:
|
||||||
|
self.collection = self._load_prompts()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reloading prompts: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_prompt(self, name: str, description: str) -> bool:
|
||||||
|
"""Add a new prompt."""
|
||||||
|
if not name or not name.strip():
|
||||||
|
raise ValueError("Prompt name cannot be empty.")
|
||||||
|
if not description or not description.strip():
|
||||||
|
raise ValueError("Prompt description cannot be empty.")
|
||||||
|
|
||||||
|
name = name.strip()
|
||||||
|
description = description.strip()
|
||||||
|
|
||||||
|
if self.collection.add_prompt(name, description):
|
||||||
|
if self._save_prompts():
|
||||||
|
# print(f"✓ Prompt '{name}' added successfully.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.collection.delete_prompt(name)
|
||||||
|
# print(f"✗ Failed to save prompt '{name}'.")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(f"✗ Prompt '{name}' already exists.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_prompt(self, name: str, description: str) -> bool:
|
||||||
|
"""Update an existing prompt."""
|
||||||
|
if not name or not name.strip():
|
||||||
|
raise ValueError("Prompt name cannot be empty.")
|
||||||
|
if not description or not description.strip():
|
||||||
|
raise ValueError("Prompt description cannot be empty.")
|
||||||
|
|
||||||
|
name = name.strip()
|
||||||
|
description = description.strip()
|
||||||
|
|
||||||
|
if self.collection.update_prompt(name, description):
|
||||||
|
if self._save_prompts():
|
||||||
|
print(f"✓ Prompt '{name}' updated successfully.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"✗ Failed to save prompt '{name}'.")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(f"✗ Prompt '{name}' does not exist.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def delete_prompt(self, name: str) -> bool:
|
||||||
|
"""Delete a prompt."""
|
||||||
|
if not name or not name.strip():
|
||||||
|
raise ValueError("Prompt name cannot be empty.")
|
||||||
|
|
||||||
|
name = name.strip()
|
||||||
|
|
||||||
|
backup_prompt = self.collection.prompts.get(name)
|
||||||
|
|
||||||
|
if self.collection.delete_prompt(name):
|
||||||
|
if self._save_prompts():
|
||||||
|
print(f"✓ Prompt '{name}' deleted successfully.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if backup_prompt:
|
||||||
|
self.collection.prompts[name] = backup_prompt
|
||||||
|
print(f"✗ Failed to save prompt '{name}'.")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(f"✗ Prompt '{name}' does not exist.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_prompt(self, name: str) -> Optional[str]:
|
||||||
|
"""Get a specific prompt description by name."""
|
||||||
|
if not name or name not in self.collection.prompts:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.collection.mark_used(name)
|
||||||
|
self._save_prompts()
|
||||||
|
|
||||||
|
return self.collection.prompts[name].description
|
||||||
|
|
||||||
|
def prompt_exists(self, name: str) -> bool:
|
||||||
|
"""Check if a prompt exists by name."""
|
||||||
|
return name in self.collection.prompts
|
||||||
|
|
||||||
|
def get_all_prompts(self) -> Dict[str, str]:
|
||||||
|
"""Get all prompts as a dictionary of name -> description."""
|
||||||
|
return self.collection.get_prompt_descriptions()
|
||||||
|
|
||||||
|
def get_prompt_names(self) -> List[str]:
|
||||||
|
return self.collection.get_prompt_names()
|
||||||
|
|
||||||
|
def get_prompt_details(self, name: str) -> Optional[PromptItem]:
|
||||||
|
"""Get prompt details by name."""
|
||||||
|
return self.collection.prompts.get(name)
|
||||||
|
|
||||||
|
def search_prompts(self, query: str) -> Dict[str, str]:
|
||||||
|
query = query.lower().strip()
|
||||||
|
if not query:
|
||||||
|
return self.get_all_prompts()
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
for name, prompt in self.collection.prompts.items():
|
||||||
|
if (query in name.lower() or
|
||||||
|
query in prompt.description.lower()):
|
||||||
|
results[name] = prompt.description
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_recently_used_prompts(self, limit: int = 5) -> Dict[str, str]:
|
||||||
|
"""Get a list of recently used prompts."""
|
||||||
|
sorted_prompts = sorted(
|
||||||
|
self.collection.prompts.items(),
|
||||||
|
key=lambda x: x[1].last_used or "",
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
for name, prompt in sorted_prompts[:limit]:
|
||||||
|
if prompt.last_used:
|
||||||
|
results[name] = prompt.description
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def count_prompts(self) -> int:
|
||||||
|
return len(self.collection.prompts)
|
||||||
|
|
||||||
|
def export_prompts(self, file_path: str, file_format: str = "json") -> bool:
|
||||||
|
"""Export prompts to file"""
|
||||||
|
try:
|
||||||
|
export_path = Path(file_path)
|
||||||
|
|
||||||
|
if file_format.lower() == "json":
|
||||||
|
with open(export_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self.collection.to_dict(), f, indent=2, ensure_ascii=False)
|
||||||
|
elif file_format.lower() == "txt":
|
||||||
|
with open(export_path, 'w', encoding='utf-8') as f:
|
||||||
|
for name, prompt in self.collection.prompts.items():
|
||||||
|
f.write(f"=== {name} ===\n")
|
||||||
|
f.write(f"{prompt.description}\n")
|
||||||
|
f.write("-" * 50 + "\n\n")
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported export format. Use 'json' or 'txt'.")
|
||||||
|
print(f"✓ Prompts exported successfully to {export_path}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error exporting prompts: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def import_prompts(self, file_path: str, merge: bool = True) -> bool:
|
||||||
|
"""Import prompts from file"""
|
||||||
|
try:
|
||||||
|
import_path = Path(file_path)
|
||||||
|
|
||||||
|
with open(import_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
imported_collection = PromptsCollection.from_dict(data)
|
||||||
|
|
||||||
|
if merge:
|
||||||
|
conflicts = []
|
||||||
|
for name, prompt in imported_collection.prompts.items():
|
||||||
|
if name in self.collection.prompts:
|
||||||
|
conflicts.append(name)
|
||||||
|
else:
|
||||||
|
self.collection.prompts[name] = prompt
|
||||||
|
|
||||||
|
if conflicts:
|
||||||
|
print(
|
||||||
|
f"⚠️ {len(conflicts)} Conflicts detected for prompts: {', '.join(conflicts)}. Skipping these.")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"✓ Imported {len(imported_collection.prompts) - len(conflicts)} prompts with {len(conflicts)} conflicts.")
|
||||||
|
else:
|
||||||
|
self.collection = imported_collection
|
||||||
|
print(f"✓ Imported {len(imported_collection.prompts)} prompts, replacing existing collection.")
|
||||||
|
|
||||||
|
return self._save_prompts()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error importing prompts: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_prompts_summary(self) -> dict:
|
||||||
|
"""Get a summary of current prompts."""
|
||||||
|
recently_used = len(self.get_recently_used_prompts())
|
||||||
|
total = self.count_prompts()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Total Prompts": total,
|
||||||
|
"Recently Used": recently_used,
|
||||||
|
"Unused": total - recently_used,
|
||||||
|
"Collection Version": self.collection.version
|
||||||
|
}
|
190
src/srtify/core/settings.py
Normal file
190
src/srtify/core/settings.py
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
from srtify.core.models import AppSettings, ApiConfig, DirectoryConfig, TranslationConfig
|
||||||
|
from srtify.utils.paths import get_config_path
|
||||||
|
|
||||||
|
|
||||||
|
home_dir = Path.home()
|
||||||
|
DEFAULT_INPUT_DIR = str(home_dir / "Documents" / "Subtitles")
|
||||||
|
DEFAULT_OUTPUT_DIR = str(home_dir / "Documents" / "Subtitles" / "Translated")
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsManager:
|
||||||
|
"""Settings manager with dataclass-based configuration."""
|
||||||
|
|
||||||
|
def __init__(self, config_file: str = None):
|
||||||
|
self.config_file = Path(config_file) if config_file else get_config_path()
|
||||||
|
self.settings = self._load_settings()
|
||||||
|
|
||||||
|
def _load_settings(self) -> AppSettings:
|
||||||
|
"""Load settings from file or create defaults."""
|
||||||
|
self.config_file.parent.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
default_settings = AppSettings(
|
||||||
|
api=ApiConfig(),
|
||||||
|
directories=DirectoryConfig(
|
||||||
|
input_dir=DEFAULT_INPUT_DIR,
|
||||||
|
output_dir=DEFAULT_OUTPUT_DIR
|
||||||
|
),
|
||||||
|
translation=TranslationConfig()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.config_file.exists():
|
||||||
|
self._save_settings(default_settings)
|
||||||
|
return default_settings
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return AppSettings.from_dict(data)
|
||||||
|
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
||||||
|
print(f"Error loading settings: {e}. Using defaults.")
|
||||||
|
backup_file = self.config_file.with_suffix('.json.backup')
|
||||||
|
if self.config_file.exists():
|
||||||
|
self.config_file.rename(backup_file)
|
||||||
|
print(f"Backed up corrupted config to {backup_file}")
|
||||||
|
|
||||||
|
self._save_settings(default_settings)
|
||||||
|
return default_settings
|
||||||
|
|
||||||
|
def _save_settings(self, settings: Optional[AppSettings] = None):
|
||||||
|
"""Save current settings to file."""
|
||||||
|
if settings is None:
|
||||||
|
settings = self.settings
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(settings.to_dict(), f, indent=2, ensure_ascii=False)
|
||||||
|
return True
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
print(f"Error saving configuration: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def reload(self) -> bool:
|
||||||
|
"""Reload settings from file."""
|
||||||
|
try:
|
||||||
|
self.settings = self._load_settings()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reloading settings: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_api_key(self) -> str:
|
||||||
|
"""Get the API key."""
|
||||||
|
return self.settings.api.gemini_key
|
||||||
|
|
||||||
|
def set_api_key(self, api_key: str) -> bool:
|
||||||
|
"""Set the API key with validation."""
|
||||||
|
if not api_key or not api_key.strip():
|
||||||
|
raise ValueError("API key cannot be empty.")
|
||||||
|
|
||||||
|
self.settings.api.gemini_key = api_key.strip()
|
||||||
|
return self._save_settings()
|
||||||
|
|
||||||
|
def is_api_key_valid(self) -> bool:
|
||||||
|
"""Check if the API key is valid."""
|
||||||
|
return self.settings.api.is_valid()
|
||||||
|
|
||||||
|
def get_directories(self) -> Tuple[str, str]:
|
||||||
|
"""Get input and output directories."""
|
||||||
|
return (
|
||||||
|
self.settings.directories.input_dir,
|
||||||
|
self.settings.directories.output_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_directories(self, input_dir: str, output_dir: str) -> bool:
|
||||||
|
"""Set input and output directories with validation."""
|
||||||
|
if not input_dir or not input_dir.strip():
|
||||||
|
raise ValueError("Input directory cannot be empty.")
|
||||||
|
if not output_dir or not output_dir.strip():
|
||||||
|
raise ValueError("Output directory cannot be empty.")
|
||||||
|
|
||||||
|
new_dirs = DirectoryConfig(
|
||||||
|
input_dir=input_dir.strip(),
|
||||||
|
output_dir=output_dir.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not new_dirs.validate():
|
||||||
|
raise ValueError("One or both directories are invalid or inaccessible.")
|
||||||
|
|
||||||
|
self.settings.directories = new_dirs
|
||||||
|
return self._save_settings()
|
||||||
|
|
||||||
|
def ensure_directories_exist(self) -> bool:
|
||||||
|
"""Ensure input and output directories exist."""
|
||||||
|
return self.settings.directories.validate()
|
||||||
|
|
||||||
|
def get_translation_config(self) -> Tuple[str, int]:
|
||||||
|
"""Get translation configuration."""
|
||||||
|
return (
|
||||||
|
self.settings.translation.default_language,
|
||||||
|
self.settings.translation.batch_size
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_translation_config(self, language: str, batch_size: int) -> bool:
|
||||||
|
"""Set translation configuration with validation."""
|
||||||
|
if not language or not language.strip():
|
||||||
|
raise ValueError("Default language cannot be empty.")
|
||||||
|
|
||||||
|
if batch_size <= 0:
|
||||||
|
raise ValueError("Batch size must be a positive integer.")
|
||||||
|
|
||||||
|
self.settings.translation.default_language = language.strip()
|
||||||
|
self.settings.translation.batch_size = batch_size
|
||||||
|
return self._save_settings()
|
||||||
|
|
||||||
|
def validate_all(self) -> bool:
|
||||||
|
"""Validate all settings."""
|
||||||
|
return self.settings.validate()
|
||||||
|
|
||||||
|
def reset_to_defaults(self) -> bool:
|
||||||
|
"""Reset all settings to defaults."""
|
||||||
|
self.settings = AppSettings(
|
||||||
|
api=ApiConfig(),
|
||||||
|
directories=DirectoryConfig(
|
||||||
|
input_dir=DEFAULT_INPUT_DIR,
|
||||||
|
output_dir=DEFAULT_OUTPUT_DIR
|
||||||
|
),
|
||||||
|
translation=TranslationConfig()
|
||||||
|
)
|
||||||
|
return self._save_settings()
|
||||||
|
|
||||||
|
def export_settings(self, file_path: str) -> bool:
|
||||||
|
"""Export settings to a file."""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self.settings.to_dict(), f, indent=2, ensure_ascii=False)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error exporting settings: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def import_settings(self, file_path: str) -> bool:
|
||||||
|
"""Import settings from a file."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
new_settings = AppSettings.from_dict(data)
|
||||||
|
|
||||||
|
if new_settings.validate():
|
||||||
|
self.settings = new_settings
|
||||||
|
return self._save_settings()
|
||||||
|
else:
|
||||||
|
print("Imported settings are invalid.")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error importing settings: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_settings_summary(self) -> dict:
|
||||||
|
"""Get a summary of current settings."""
|
||||||
|
return {
|
||||||
|
"API Key": "Set" if self.is_api_key_valid() else "Not Set",
|
||||||
|
"Input Directory": self.settings.directories.input_dir,
|
||||||
|
"Output Directory": self.settings.directories.output_dir,
|
||||||
|
"Language": self.settings.translation.default_language,
|
||||||
|
"Batch Size": self.settings.translation.batch_size,
|
||||||
|
"Directories Valid": self.settings.directories.validate()
|
||||||
|
}
|
175
src/srtify/core/translator.py
Normal file
175
src/srtify/core/translator.py
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
import gemini_srt_translator as gst
|
||||||
|
from .file_encoding import detect_and_fix_encoding
|
||||||
|
from srtify.core.settings import SettingsManager
|
||||||
|
from srtify.core.prompts import PromptsManager
|
||||||
|
from srtify.utils.utils import fancy_headline
|
||||||
|
|
||||||
|
|
||||||
|
class TranslatorApp:
|
||||||
|
""" Main Application class for handling translation operations. """
|
||||||
|
def __init__(self, settings_manager: SettingsManager, prompts_manager: PromptsManager):
|
||||||
|
self.settings = settings_manager
|
||||||
|
self.prompts = prompts_manager
|
||||||
|
|
||||||
|
def translate_single_file(
|
||||||
|
self,
|
||||||
|
input_path: Path,
|
||||||
|
output_path: Path,
|
||||||
|
srt_file: str,
|
||||||
|
language: str,
|
||||||
|
prompt: str,
|
||||||
|
batch_size: int,
|
||||||
|
api_key: str
|
||||||
|
) -> bool:
|
||||||
|
""" Translate a single SRT file. """
|
||||||
|
input_file = input_path / srt_file
|
||||||
|
output_file = output_path / srt_file
|
||||||
|
|
||||||
|
print(f"Translating: {srt_file}")
|
||||||
|
print(f"Language: {language}")
|
||||||
|
print(f"Batch Size: {batch_size}")
|
||||||
|
if prompt:
|
||||||
|
print(f"Prompt: {prompt[:50]}{'...' if len(prompt) > 50 else ''}")
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
print("❌ No API key found. Configure settings first.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Configure gemini_srt_translator
|
||||||
|
gst.gemini_api_key = api_key
|
||||||
|
gst.target_language = language
|
||||||
|
gst.input_file = str(input_file)
|
||||||
|
gst.output_file = str(output_file)
|
||||||
|
gst.batch_size = batch_size
|
||||||
|
if prompt:
|
||||||
|
gst.description = prompt
|
||||||
|
|
||||||
|
try:
|
||||||
|
gst.translate()
|
||||||
|
print("✅ Translation successful.")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Translation failed for {srt_file}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_srt_file(self, input_path: Path, specific_file: Optional[str] = None) -> List[str]:
|
||||||
|
""" Get list of SRT files to process. """
|
||||||
|
if specific_file:
|
||||||
|
file_name = specific_file.strip()
|
||||||
|
if not file_name.endswith(".srt"):
|
||||||
|
file_name = f"{file_name}.srt"
|
||||||
|
|
||||||
|
file_path = input_path / file_name
|
||||||
|
if file_path.exists():
|
||||||
|
return [file_name]
|
||||||
|
else:
|
||||||
|
print(f"❌ File {file_name} not found in {input_path}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
else:
|
||||||
|
str_files = [file.name for file in input_path.glob("*.srt")]
|
||||||
|
return str_files
|
||||||
|
|
||||||
|
def validate_files_encoding(self, input_path: Path, srt_files: List[str]) -> List[str]:
|
||||||
|
""" Validate and fix file encoding for all SRT files. """
|
||||||
|
print("Checking file encoding...")
|
||||||
|
valid_files = []
|
||||||
|
|
||||||
|
for srt_file in srt_files:
|
||||||
|
if detect_and_fix_encoding(str(input_path), srt_file):
|
||||||
|
valid_files.append(srt_file)
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to fix encoding for file {srt_file}. Skipping...")
|
||||||
|
|
||||||
|
return valid_files
|
||||||
|
|
||||||
|
def resolve_prompt(self, options: Dict[str, Any]) -> Optional[str]:
|
||||||
|
""" Resolve which prompt to use based on options """
|
||||||
|
if options.get('custom_prompt'):
|
||||||
|
return options['custom_prompt'].strip()
|
||||||
|
elif options.get('prompts.py'):
|
||||||
|
results = self.prompts.search_prompts(options['prompt'].strip())
|
||||||
|
if results:
|
||||||
|
if len(results) == 1:
|
||||||
|
return list(results.values())[0]
|
||||||
|
else:
|
||||||
|
print(f"Found {len(results)} prompts matching '{options['prompt']}'")
|
||||||
|
for name, desc in results.items():
|
||||||
|
print(f" - {name}: {desc[:100]}{'...' if len(desc) > 100 else ''}")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
print(f"No prompts found matching '{options['prompt']}'")
|
||||||
|
return None
|
||||||
|
elif options.get('quick'):
|
||||||
|
return "Translate naturally and accurately"
|
||||||
|
else:
|
||||||
|
return "Translate naturally and accurately"
|
||||||
|
|
||||||
|
def run_translation(self, options: Dict[str, Any]) -> None:
|
||||||
|
""" Main translation runner. """
|
||||||
|
input_dir, output_dir = self.settings.get_directories()
|
||||||
|
default_lang, default_batch_size = self.settings.get_translation_config()
|
||||||
|
api_key = self.settings.get_api_key()
|
||||||
|
|
||||||
|
input_path = Path(options.get('input_path', input_dir))
|
||||||
|
output_path = Path(options.get('output_path', output_dir))
|
||||||
|
language = options.get('language', default_lang)
|
||||||
|
batch_size = options.get('batch_size', default_batch_size)
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
print("❌ No API key found. Use 'subtitle-translator settings' to configure.")
|
||||||
|
return
|
||||||
|
|
||||||
|
selected_prompt = self.resolve_prompt(options)
|
||||||
|
if selected_prompt is None:
|
||||||
|
print("❌ No prompt selected. Exiting.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Print configuration
|
||||||
|
print("Translation Settings:")
|
||||||
|
print("=" * 30)
|
||||||
|
print(f"- Target Language: {language}")
|
||||||
|
print(f"- Input Directory: {input_path}")
|
||||||
|
print(f"- Output Directory: {output_path}")
|
||||||
|
print(f"- Batch Size: {batch_size}")
|
||||||
|
print(f"- API Key: {'Set' if api_key else 'Not Set'}")
|
||||||
|
print("=" * 30)
|
||||||
|
|
||||||
|
srt_files = self.get_srt_file(input_path, options.get('file'))
|
||||||
|
if not srt_files:
|
||||||
|
print("❌ No SRT files found. Exiting.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Found {len(srt_files)} SRT files.")
|
||||||
|
|
||||||
|
valid_files = self.validate_files_encoding(input_path, srt_files)
|
||||||
|
if not valid_files:
|
||||||
|
print("❌ No valid SRT files found after encoding check. Exiting.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Found {len(valid_files)} valid SRT files.")
|
||||||
|
print(f"\nStarting translation to {language}...")
|
||||||
|
print("=" * shutil.get_terminal_size().columns)
|
||||||
|
|
||||||
|
successful = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
for srt_file in valid_files:
|
||||||
|
if self.translate_single_file(
|
||||||
|
input_path, output_path, srt_file,
|
||||||
|
language, selected_prompt, batch_size, api_key
|
||||||
|
):
|
||||||
|
successful += 1
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
print("=" * shutil.get_terminal_size().columns)
|
||||||
|
print(fancy_headline("TRANSLATION SUMMARY", "rounded"))
|
||||||
|
print(f"Successful: {successful}")
|
||||||
|
print(f"Failed: {failed}")
|
||||||
|
print(f"Output Directory: {output_path}")
|
0
src/srtify/utils/__init__.py
Normal file
0
src/srtify/utils/__init__.py
Normal file
22
src/srtify/utils/paths.py
Normal file
22
src/srtify/utils/paths.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from pathlib import Path
|
||||||
|
import platformdirs
|
||||||
|
|
||||||
|
|
||||||
|
APP_NAME = "srtify"
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_dir() -> Path:
|
||||||
|
""" Get platform-specific application directory """
|
||||||
|
app_dir = Path(platformdirs.user_data_dir(APP_NAME))
|
||||||
|
app_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
return app_dir
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_path() -> Path:
|
||||||
|
""" Get path to config file """
|
||||||
|
return get_app_dir() / "config.json"
|
||||||
|
|
||||||
|
|
||||||
|
def get_prompts_path() -> Path:
|
||||||
|
""" Get path to prompts file """
|
||||||
|
return get_app_dir() / "prompts.json"
|
51
src/srtify/utils/utils.py
Normal file
51
src/srtify/utils/utils.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def clear_screen():
|
||||||
|
if os.name == 'nt':
|
||||||
|
os.system('cls')
|
||||||
|
else:
|
||||||
|
os.system('clear')
|
||||||
|
|
||||||
|
|
||||||
|
def fancy_headline(text, style='double'):
|
||||||
|
"""
|
||||||
|
Create headlines with various border styles using Unicode characters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The headline text
|
||||||
|
style: Border style ('double', 'heavy', 'rounded', 'dashed')
|
||||||
|
"""
|
||||||
|
# Define different border character sets
|
||||||
|
border_styles = {
|
||||||
|
'double': {
|
||||||
|
'top_left': '╔', 'top_right': '╗', 'bottom_left': '╚', 'bottom_right': '╝',
|
||||||
|
'horizontal': '═', 'vertical': '║'
|
||||||
|
},
|
||||||
|
'heavy': {
|
||||||
|
'top_left': '┏', 'top_right': '┓', 'bottom_left': '┗', 'bottom_right': '┛',
|
||||||
|
'horizontal': '━', 'vertical': '┃'
|
||||||
|
},
|
||||||
|
'rounded': {
|
||||||
|
'top_left': '╭', 'top_right': '╮', 'bottom_left': '╰', 'bottom_right': '╯',
|
||||||
|
'horizontal': '─', 'vertical': '│'
|
||||||
|
},
|
||||||
|
'dashed': {
|
||||||
|
'top_left': '┌', 'top_right': '┐', 'bottom_left': '└', 'bottom_right': '┘',
|
||||||
|
'horizontal': '┄', 'vertical': '┆'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the border characters for the selected style
|
||||||
|
borders = border_styles.get(style, border_styles['double'])
|
||||||
|
|
||||||
|
# Calculate dimensions
|
||||||
|
text_length = len(text)
|
||||||
|
total_width = text_length + 4 # 2 spaces padding + 2 border chars
|
||||||
|
|
||||||
|
# Build the headline
|
||||||
|
top_line = borders['top_left'] + borders['horizontal'] * (total_width - 2) + borders['top_right']
|
||||||
|
middle_line = borders['vertical'] + ' ' + text + ' ' + borders['vertical']
|
||||||
|
bottom_line = borders['bottom_left'] + borders['horizontal'] * (total_width - 2) + borders['bottom_right']
|
||||||
|
|
||||||
|
return f"{top_line}\n{middle_line}\n{bottom_line}"
|
472
uv.lock
generated
Normal file
472
uv.lock
generated
Normal file
|
@ -0,0 +1,472 @@
|
||||||
|
version = 1
|
||||||
|
revision = 2
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "sniffio" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cachetools"
|
||||||
|
version = "5.5.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2025.6.15"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chardet"
|
||||||
|
version = "5.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.2.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gemini-srt-translator"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "google-genai" },
|
||||||
|
{ name = "json-repair" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "srt" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/20/76/fb4b63a77e42e29a5a465ed73199a02bd6f4bd64ad0929bd322fb80a7ed3/gemini_srt_translator-2.1.3.tar.gz", hash = "sha256:c53579451eead2d2ad166a4868b1efe06e2676f4823e8b340178ecedbc597b1d", size = 32348, upload-time = "2025-07-01T18:47:05.486Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/bb/6ccc49aaddaebea9b9a70345f2c8d25d87e2cb2a0332b6e29c7e4f3195f0/gemini_srt_translator-2.1.3-py3-none-any.whl", hash = "sha256:e787c060528d2797349073b9a7ef0fc751745741f4e5ad7bddb6051307e00ad7", size = 30362, upload-time = "2025-07-01T18:47:03.403Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-auth"
|
||||||
|
version = "2.40.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cachetools" },
|
||||||
|
{ name = "pyasn1-modules" },
|
||||||
|
{ name = "rsa" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-genai"
|
||||||
|
version = "1.23.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "google-auth" },
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "tenacity" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "websockets" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/86/8a/ac6f0efa0a874072b87ea13a827d549116c7c78ade93042abf0a3492039f/google_genai-1.23.0.tar.gz", hash = "sha256:a3ce7803ac3b038d4c4e166070451b560278c9f20cd819650debd17cd69cb1b3", size = 223825, upload-time = "2025-06-27T23:45:56.828Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/b1/9bc286d0880173ac81b72b12363eea54db552265933bec5f5b3c8385cc44/google_genai-1.23.0-py3-none-any.whl", hash = "sha256:59d603e35440fb1e61482264ee214d17f8c4ed9d29ec08cd4a6de8d090aaabb9", size = 223845, upload-time = "2025-06-27T23:45:55.442Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "httpcore" },
|
||||||
|
{ name = "idna" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "json-repair"
|
||||||
|
version = "0.47.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ae/9e/e8bcda4fd47b16fcd4f545af258d56ba337fa43b847beb213818d7641515/json_repair-0.47.6.tar.gz", hash = "sha256:4af5a14b9291d4d005a11537bae5a6b7912376d7584795f0ac1b23724b999620", size = 34400, upload-time = "2025-07-01T15:42:07.458Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/f8/f464ce2afc4be5decf53d0171c2d399d9ee6cd70d2273b8e85e7c6d00324/json_repair-0.47.6-py3-none-any.whl", hash = "sha256:1c9da58fb6240f99b8405f63534e08f8402793f09074dea25800a0b232d4fb19", size = 25754, upload-time = "2025-07-01T15:42:06.418Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "25.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.3.8"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyasn1"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyasn1-modules"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyasn1" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "2.11.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.33.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.32.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rsa"
|
||||||
|
version = "4.9.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyasn1" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "80.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simple-term-menu"
|
||||||
|
version = "1.6.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/80/f0f10b4045628645a841d3d98b584a8699005ee03a211fc7c45f6c6f0e99/simple_term_menu-1.6.6.tar.gz", hash = "sha256:9813d36f5749d62d200a5599b1ec88469c71378312adc084c00c00bfbb383893", size = 35493, upload-time = "2024-12-02T16:31:50.639Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/09/21d993e394c1fe5c44cd90453d88ed44932da8dfca006e424c072d77d29b/simple_term_menu-1.6.6-py3-none-any.whl", hash = "sha256:c2a869efa7a9f7e4a9c25858b42ca6974034951c137d5e281f5339b06ed8c9c2", size = 27600, upload-time = "2024-12-02T16:31:48.934Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniffio"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "srt"
|
||||||
|
version = "3.5.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/b7/4a1bc231e0681ebf339337b0cd05b91dc6a0d701fa852bb812e244b7a030/srt-3.5.3.tar.gz", hash = "sha256:4884315043a4f0740fd1f878ed6caa376ac06d70e135f306a6dc44632eed0cc0", size = 28296, upload-time = "2023-03-28T02:35:44.007Z" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "srtify"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "chardet" },
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "gemini-srt-translator" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "setuptools" },
|
||||||
|
{ name = "simple-term-menu" },
|
||||||
|
{ name = "wheel" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "chardet", specifier = ">=5.2.0" },
|
||||||
|
{ name = "click", specifier = ">=8.2.1" },
|
||||||
|
{ name = "gemini-srt-translator", specifier = ">=2.1.3" },
|
||||||
|
{ name = "platformdirs", specifier = ">=4.3.8" },
|
||||||
|
{ name = "setuptools", specifier = ">=65.5.1" },
|
||||||
|
{ name = "simple-term-menu", specifier = ">=1.6.6" },
|
||||||
|
{ name = "wheel", specifier = ">=0.45.1" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tenacity"
|
||||||
|
version = "8.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309, upload-time = "2024-07-05T07:25:31.836Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165, upload-time = "2024-07-05T07:25:29.591Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.14.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "websockets"
|
||||||
|
version = "15.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wheel"
|
||||||
|
version = "0.45.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" },
|
||||||
|
]
|
Loading…
Add table
Add a link
Reference in a new issue