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