Skip to main content

psyche_subtitle_toolkit/
lib.rs

1//! # psyche-subtitle-toolkit
2//!
3//! Extract, translate, and mux ASS subtitles in MKV files.
4//! Built for [Psyche](https://github.com/Gitlawb/psyche) but usable as a standalone CLI or library.
5//!
6//! No cloud required. No telemetry. Every translation provider is opt-in.
7//!
8//! ## Supported providers
9//!
10//! | Provider | Struct | Endpoint |
11//! |----------|--------|----------|
12//! | [Ollama](https://ollama.com) | [`OllamaTranslator`] | `/api/generate` |
13//! | [OpenAI](https://platform.openai.com) | [`OpenAiTranslator`] | `/v1/chat/completions` |
14//! | [OpenRouter](https://openrouter.ai) | [`OpenRouterTranslator`] | `/api/v1/chat/completions` |
15//! | [Anthropic](https://docs.anthropic.com) | [`AnthropicTranslator`] | `/v1/messages` |
16//! | [DeepL](https://www.deepl.com) | [`DeepLTranslator`] | `/v2/translate` |
17//! | [Google Translate](https://cloud.google.com/translate) | [`GoogleTranslator`] | `/language/translate/v2` |
18//! | [Gemini](https://ai.google.dev/gemini-api/docs) | [`GeminiTranslator`] | `v1beta/models/{model}:generateContent` |
19//!
20//! ## Quick start (library)
21//!
22//! Translate an MKV file in-place:
23//!
24//! ```no_run
25//! use std::sync::Arc;
26//! use psyche_subtitle_toolkit::{translate_mkv, TranslateMkvOptions, OllamaTranslator, Translator};
27//!
28//! # async fn example() -> psyche_subtitle_toolkit::Result<()> {
29//! let translator: Arc<dyn Translator> = Arc::new(OllamaTranslator::new("llama3.1")?);
30//! translate_mkv(
31//!     TranslateMkvOptions {
32//!         input: "/media/anime/episode.mkv".into(),
33//!         target_language: "pt-BR".into(),
34//!         track_id: None,
35//!         keep_temp: false,
36//!         dry_run: false,
37//!         source_language: None,
38//!         resume: false,
39//!         max_concurrent: 1,
40//!     },
41//!     translator,
42//! ).await?;
43//! # Ok(())
44//! # }
45//! ```
46//!
47//! Translate ASS content directly (no MKV I/O):
48//!
49//! ```no_run
50//! use std::sync::Arc;
51//! use psyche_subtitle_toolkit::{translate_ass, AssSubtitle, OllamaTranslator, Translator};
52//!
53//! # async fn example() -> psyche_subtitle_toolkit::Result<()> {
54//! let ass = AssSubtitle::parse(&std::fs::read_to_string("source.ass")?)?;
55//! let translator: Arc<dyn Translator> = Arc::new(OllamaTranslator::new("llama3.1")?);
56//! let translated = translate_ass(ass, "pt-BR", Some("en"), 1, translator).await?;
57//! std::fs::write("translated.ass", translated.render())?;
58//! # Ok(())
59//! # }
60//! ```
61//!
62//! Implement a custom provider:
63//!
64//! ```
65//! use async_trait::async_trait;
66//! use psyche_subtitle_toolkit::{Translator, TranslationRequest, Result};
67//!
68//! struct MyTranslator;
69//!
70//! #[async_trait]
71//! impl Translator for MyTranslator {
72//!     async fn translate(&self, request: TranslationRequest<'_>) -> Result<String> {
73//!         // Your translation logic here.
74//!         // request.source_text is numbered: "<1> hello\n<2> world"
75//!         // Return translated text in the same format.
76//!         Ok(request.source_text.to_string())
77//!     }
78//! }
79//! ```
80//!
81//! ## Pipeline overview
82//!
83//! 1. **Inspect** — `mkvmerge -J` identifies tracks, selects the ASS subtitle
84//! 2. **Extract** — `mkvextract tracks` pulls the ASS file to a temp directory
85//! 3. **Parse** — ASS parser reads dialogue lines, preserving headers and styles
86//! 4. **Strip tags** — Override tags (`{\pos(...)}`, `{\an7}`) removed and stored
87//! 5. **Chunk** — Cues split into 200-line batches
88//! 6. **Translate** — Each chunk sent to the provider (concurrent if `max_concurrent > 1`)
89//! 7. **Retry** — Failed chunks retried up to 3 times with exponential backoff
90//! 8. **Apply** — Translated text mapped back to cues by ID
91//! 9. **Reinject tags** — Original override tags prepended back
92//! 10. **Mux** — `mkvmerge` replaces the original subtitle track in-place
93
94#![forbid(unsafe_code)]
95
96/// Error types for the subtitle toolkit.
97pub mod error;
98/// MKV container inspection and manipulation (mkvmerge/mkvextract wrappers).
99pub mod media;
100/// Translation pipeline: MKV full-pipeline and subtitle-only translation.
101pub mod pipeline;
102mod retry;
103/// Subtitle parsing, chunking, and tag manipulation.
104pub mod subtitles;
105/// Pluggable translation providers and the [`Translator`] trait.
106pub mod translation;
107
108// Error types
109pub use error::{Result, SubtitleToolkitError};
110
111// MKV inspection
112pub use media::mkv::{MkvInfo, MkvTrack, MkvTrackProperties, inspect_mkv, select_ass_track};
113
114// Pipeline
115pub use pipeline::{TranslateMkvOptions, dry_run_summary, translate_ass, translate_mkv};
116
117// Subtitle model
118pub use subtitles::ass::AssSubtitle;
119pub use subtitles::model::{SubtitleCue, SubtitleDocument};
120pub use subtitles::structured::{
121    apply_translation, chunk_document, chunk_document_by_lines, parse_numbered_text,
122    reinject_tags, strip_tags, to_numbered_text,
123};
124
125// Translation providers
126pub use translation::anthropic::AnthropicTranslator;
127pub use translation::deepl::DeepLTranslator;
128pub use translation::gemini::GeminiTranslator;
129pub use translation::google::GoogleTranslator;
130pub use translation::ollama::OllamaTranslator;
131pub use translation::openai::OpenAiTranslator;
132pub use translation::openrouter::OpenRouterTranslator;
133pub use translation::{TranslationRequest, Translator};