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//!         resume: false,
38//!         max_concurrent: 1,
39//!     },
40//!     translator,
41//! ).await?;
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! Translate ASS content directly (no MKV I/O):
47//!
48//! ```no_run
49//! use std::sync::Arc;
50//! use psyche_subtitle_toolkit::{translate_ass, AssSubtitle, OllamaTranslator, Translator};
51//!
52//! # async fn example() -> psyche_subtitle_toolkit::Result<()> {
53//! let ass = AssSubtitle::parse(&std::fs::read_to_string("source.ass")?)?;
54//! let translator: Arc<dyn Translator> = Arc::new(OllamaTranslator::new("llama3.1")?);
55//! let translated = translate_ass(ass, "pt-BR", 1, translator).await?;
56//! std::fs::write("translated.ass", translated.render())?;
57//! # Ok(())
58//! # }
59//! ```
60//!
61//! Implement a custom provider:
62//!
63//! ```
64//! use async_trait::async_trait;
65//! use psyche_subtitle_toolkit::{Translator, TranslationRequest, Result};
66//!
67//! struct MyTranslator;
68//!
69//! #[async_trait]
70//! impl Translator for MyTranslator {
71//!     async fn translate(&self, request: TranslationRequest<'_>) -> Result<String> {
72//!         // Your translation logic here.
73//!         // request.source_text is numbered: "<1> hello\n<2> world"
74//!         // Return translated text in the same format.
75//!         Ok(request.source_text.to_string())
76//!     }
77//! }
78//! ```
79//!
80//! ## Pipeline overview
81//!
82//! 1. **Inspect** — `mkvmerge -J` identifies tracks, selects the ASS subtitle
83//! 2. **Extract** — `mkvextract tracks` pulls the ASS file to a temp directory
84//! 3. **Parse** — ASS parser reads dialogue lines, preserving headers and styles
85//! 4. **Strip tags** — Override tags (`{\pos(...)}`, `{\an7}`) removed and stored
86//! 5. **Chunk** — Cues split into 200-line batches
87//! 6. **Translate** — Each chunk sent to the provider (concurrent if `max_concurrent > 1`)
88//! 7. **Retry** — Failed chunks retried up to 3 times with exponential backoff
89//! 8. **Apply** — Translated text mapped back to cues by ID
90//! 9. **Reinject tags** — Original override tags prepended back
91//! 10. **Mux** — `mkvmerge` replaces the original subtitle track in-place
92
93#![forbid(unsafe_code)]
94
95/// Error types for the subtitle toolkit.
96pub mod error;
97/// MKV container inspection and manipulation (mkvmerge/mkvextract wrappers).
98pub mod media;
99/// Translation pipeline: MKV full-pipeline and subtitle-only translation.
100pub mod pipeline;
101mod retry;
102/// Subtitle parsing, chunking, and tag manipulation.
103pub mod subtitles;
104/// Pluggable translation providers and the [`Translator`] trait.
105pub mod translation;
106
107// Error types
108pub use error::{Result, SubtitleToolkitError};
109
110// MKV inspection
111pub use media::mkv::{
112    MkvInfo, MkvTrack, MkvTrackProperties, SubtitleFormat, inspect_mkv, select_ass_track,
113    select_subtitle_track,
114};
115
116// Pipeline
117pub use pipeline::{
118    TranslateMkvOptions, dry_run_summary, translate_ass, translate_mkv, translate_srt,
119    translate_vtt,
120};
121
122// Subtitle model
123pub use subtitles::ass::AssSubtitle;
124pub use subtitles::srt::SrtSubtitle;
125pub use subtitles::vtt::VttSubtitle;
126pub use subtitles::model::{SubtitleCue, SubtitleDocument};
127pub use subtitles::structured::{
128    apply_translation, chunk_document, chunk_document_by_lines, parse_numbered_text,
129    reinject_tags, strip_tags, to_numbered_text,
130};
131
132// Translation providers
133pub use translation::anthropic::AnthropicTranslator;
134pub use translation::deepl::DeepLTranslator;
135pub use translation::gemini::GeminiTranslator;
136pub use translation::google::GoogleTranslator;
137pub use translation::ollama::OllamaTranslator;
138pub use translation::openai::OpenAiTranslator;
139pub use translation::openrouter::OpenRouterTranslator;
140pub use translation::{TranslationRequest, Translator};