Skip to content

Commit f20575d

Browse files
committed
feat: v0.1.12 — translate custom prompt + min 3min chapters + min 15s viral clips
1 parent c38cc7e commit f20575d

9 files changed

Lines changed: 69 additions & 22 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "my-media-kit",
33
"private": true,
4-
"version": "0.1.11",
4+
"version": "0.1.12",
55
"type": "module",
66
"scripts": {
77
"tauri": "tauri",

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ members = [
1010
]
1111

1212
[workspace.package]
13-
version = "0.1.11"
13+
version = "0.1.12"
1414
edition = "2021"
1515
authors = ["phucnt"]
1616
license = "MIT"

src-tauri/crates/content-kit/src/chapters.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ pub fn system_prompt(language: &str, summary_hint: Option<&str>) -> String {
3131
format!(
3232
"You create YouTube chapter lists. Respond in {language}. First \
3333
chapter must start at 00:00. Keep titles short (under 8 words), \
34-
meaningful, and descriptive of the upcoming section. Aim for 5-10 \
35-
chapters for a 10-minute video; scale with length.{hint}"
34+
meaningful, and descriptive of the upcoming section. \
35+
MINIMUM chapter length: 3 minutes (180000 ms) — do NOT create \
36+
chapters shorter than this. Merge short sections into broader \
37+
topics instead of splitting aggressively.{hint}"
3638
)
3739
}
3840

@@ -134,6 +136,18 @@ impl<'a> ChapterRunner for ProviderChapterRunner<'a> {
134136
first.timestamp_ms = 0;
135137
}
136138

139+
// Enforce minimum 3-minute gap between chapters. Drop any chapter
140+
// that starts less than 180_000 ms after the previously kept one.
141+
const MIN_GAP_MS: i64 = 180_000;
142+
let mut filtered: Vec<Chapter> = Vec::with_capacity(chapters.len());
143+
for c in chapters {
144+
match filtered.last() {
145+
Some(prev) if c.timestamp_ms - prev.timestamp_ms < MIN_GAP_MS => {}
146+
_ => filtered.push(c),
147+
}
148+
}
149+
chapters = filtered;
150+
137151
Ok(ChapterList {
138152
language: language.into(),
139153
chapters,
@@ -181,16 +195,16 @@ mod tests {
181195
response: json!({
182196
"chapters": [
183197
{ "timestampMs": 3_000, "title": "Intro" },
184-
{ "timestampMs": 60_000, "title": "Main" }
198+
{ "timestampMs": 240_000, "title": "Main" }
185199
]
186200
}),
187201
};
188202
let runner = ProviderChapterRunner { provider: &stub };
189-
let segments = vec![TranscriptionSegment::new(0, 120_000, "full talk")];
190-
let list = runner.run(&segments, "English", "claude-3").await.unwrap();
203+
let segments = vec![TranscriptionSegment::new(0, 600_000, "full talk")];
204+
let list = runner.run(&segments, "English", "claude-3", None).await.unwrap();
191205
assert_eq!(list.chapters.len(), 2);
192206
assert_eq!(list.chapters[0].timestamp_ms, 0);
193-
assert_eq!(list.chapters[1].timestamp_ms, 60_000);
207+
assert_eq!(list.chapters[1].timestamp_ms, 240_000);
194208
}
195209

196210
#[tokio::test]
@@ -201,12 +215,12 @@ mod tests {
201215
"chapters": [
202216
{ "timestampMs": 0, "title": "a" },
203217
{ "timestampMs": 0, "title": "a dup" },
204-
{ "timestampMs": 5_000, "title": "b" }
218+
{ "timestampMs": 200_000, "title": "b" }
205219
]
206220
}),
207221
};
208222
let runner = ProviderChapterRunner { provider: &stub };
209-
let list = runner.run(&[], "English", "claude-3").await.unwrap();
223+
let list = runner.run(&[], "English", "claude-3", None).await.unwrap();
210224
assert_eq!(list.chapters.len(), 2);
211225
}
212226
}

src-tauri/crates/content-kit/src/translate.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,25 @@ fn primary_tag(tag: &str) -> &str {
7878

7979
/// `summary_hint` — brief content description injected into the system prompt so
8080
/// the model can maintain consistent terminology and proper-noun romanization.
81-
pub fn system_prompt(target_language_name: &str, summary_hint: Option<&str>) -> String {
81+
pub fn system_prompt(
82+
target_language_name: &str,
83+
summary_hint: Option<&str>,
84+
custom_instruction: Option<&str>,
85+
) -> String {
8286
let hint_section = summary_hint
8387
.filter(|h| !h.trim().is_empty())
8488
.map(|h| format!(
8589
"\nContent context (use for consistent terminology and proper-noun \
8690
romanization across all batches): {h}"
8791
))
8892
.unwrap_or_default();
93+
let custom_section = custom_instruction
94+
.filter(|h| !h.trim().is_empty())
95+
.map(|h| format!("\nUser style/context instruction (follow this when translating): {h}"))
96+
.unwrap_or_default();
8997
format!(
9098
"You are a professional subtitle translator. Your ONLY job is to translate \
91-
text into {target_language_name}.{hint_section}\n\
99+
text into {target_language_name}.{hint_section}{custom_section}\n\
92100
CRITICAL RULES:\n\
93101
- Every output string MUST be written entirely in {target_language_name}.\n\
94102
- Do NOT output Chinese characters, Japanese characters, or any other script.\n\
@@ -249,6 +257,7 @@ impl<'a> TranslateRunner for ProviderTranslateRunner<'a> {
249257
batch,
250258
target_name,
251259
None,
260+
None,
252261
&prev_context,
253262
)
254263
.await?;
@@ -297,6 +306,7 @@ pub async fn translate_batch_with_retry(
297306
batch: &TranscriptBatch,
298307
target_name: &str,
299308
summary_hint: Option<&str>,
309+
custom_instruction: Option<&str>,
300310
prev_context: &[String],
301311
) -> Result<Vec<String>, AiProviderError> {
302312
let want = batch.segments.len();
@@ -331,7 +341,7 @@ pub async fn translate_batch_with_retry(
331341
max_tokens: 4096,
332342
..CompletionRequest::structured(
333343
model,
334-
system_prompt(target_name, summary_hint),
344+
system_prompt(target_name, summary_hint, custom_instruction),
335345
user,
336346
"TranslatedBatch",
337347
response_schema(),
@@ -480,10 +490,12 @@ mod tests {
480490

481491
#[test]
482492
fn system_prompt_includes_summary_hint() {
483-
let p = system_prompt("Vietnamese", Some("A talk about rockets"));
493+
let p = system_prompt("Vietnamese", Some("A talk about rockets"), None);
484494
assert!(p.contains("A talk about rockets"));
485-
let p_no_hint = system_prompt("Vietnamese", None);
495+
let p_no_hint = system_prompt("Vietnamese", None, None);
486496
assert!(!p_no_hint.contains("Content context"));
497+
let p_custom = system_prompt("Vietnamese", None, Some("formal tone"));
498+
assert!(p_custom.contains("formal tone"));
487499
}
488500

489501
#[test]

src-tauri/crates/content-kit/src/viral_clips.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ pub fn system_prompt(language: &str, summary_hint: Option<&str>) -> String {
3131
format!(
3232
"You are a social media content strategist. Analyze the transcript \
3333
and find 3-5 segments that would make the best short-form clips \
34-
(15-60 seconds each) for YouTube Shorts, TikTok, or Reels.\n\n\
34+
for YouTube Shorts, TikTok, or Reels. Each clip MUST be AT LEAST \
35+
15 seconds long (endMs - startMs >= 15000) and at most 60 seconds. \
36+
Extend the selection to include enough context if needed to reach \
37+
the 15-second minimum.\n\n\
3538
For each clip provide:\n\
3639
- Precise start and end timestamps (in milliseconds)\n\
3740
- A hook explaining why this moment is engaging (emotional peak, \
@@ -133,14 +136,23 @@ impl<'a> ViralClipRunner for ProviderViralClipRunner<'a> {
133136
let parsed: ClipResponse =
134137
serde_json::from_value(value).map_err(|e| AiProviderError::Malformed(e.to_string()))?;
135138

139+
// Enforce 15s minimum — extend end_ms if the model returned a shorter clip.
140+
const MIN_CLIP_MS: i64 = 15_000;
141+
let transcript_end = segments.last().map(|s| s.end_ms).unwrap_or(i64::MAX);
136142
let mut clips: Vec<ViralClip> = parsed
137143
.clips
138144
.into_iter()
139-
.map(|e| ViralClip {
140-
start_ms: e.start_ms,
141-
end_ms: e.end_ms,
142-
hook: e.hook,
143-
caption: e.caption,
145+
.map(|e| {
146+
let mut end_ms = e.end_ms;
147+
if end_ms - e.start_ms < MIN_CLIP_MS {
148+
end_ms = (e.start_ms + MIN_CLIP_MS).min(transcript_end);
149+
}
150+
ViralClip {
151+
start_ms: e.start_ms,
152+
end_ms,
153+
hook: e.hook,
154+
caption: e.caption,
155+
}
144156
})
145157
.collect();
146158

src-tauri/src/commands/content.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ pub struct TranslateCommandRequest {
9292
/// can maintain consistent terminology across batches.
9393
#[serde(default)]
9494
pub summary_hint: Option<String>,
95+
/// Optional free-form user instruction to adjust style/context (tone, terminology, etc.).
96+
#[serde(default)]
97+
pub custom_instruction: Option<String>,
9598
}
9699

97100
/// Tauri event emitted after each batch finishes. Payload: `{ batch, total, percent }`.
@@ -144,6 +147,7 @@ pub async fn content_translate(
144147
batch,
145148
target_name,
146149
request.summary_hint.as_deref(),
150+
request.custom_instruction.as_deref(),
147151
&prev_context,
148152
)
149153
.await

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "MyMediaKit",
4-
"version": "0.1.11",
4+
"version": "0.1.12",
55
"identifier": "tech.lighton.media.MyMediaKit",
66
"build": {
77
"frontendDist": "../src"

src/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ <h3>Summary</h3>
8989
<h2>Translate</h2>
9090
<p class="hint">Translates to the output language set above. If already matching, originals returned unchanged.</p>
9191
<div class="actions">
92+
<textarea id="translate-custom-prompt" rows="2" placeholder="Custom style or context (e.g. 'keep technical terms in English', 'formal tone', 'preserve humor and idioms')…" style="width:100%;resize:vertical;margin-bottom:6px;"></textarea>
9293
<button id="btn-translate" class="primary">Translate</button>
9394
<span id="translate-status" class="status">ready</span>
9495
</div>

src/js/features/translate.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export function initTranslateView() {
2424
const btn = document.getElementById("btn-translate");
2525

2626
// ── Progress bar (created inline, same pattern as YT download) ──────
27+
const promptBox = document.getElementById("translate-custom-prompt");
28+
2729
const progressBox = document.createElement("div");
2830
progressBox.className = "progress-box";
2931
progressBox.hidden = true;
@@ -83,6 +85,7 @@ export function initTranslateView() {
8385

8486
try {
8587
const summary = getSummary();
88+
const customInstruction = promptBox?.value.trim() || null;
8689
const out = await invoke("content_translate", {
8790
request: {
8891
provider,
@@ -91,6 +94,7 @@ export function initTranslateView() {
9194
sourceLanguage: source.transcript.language ?? null,
9295
targetLanguage: target,
9396
summaryHint: summary?.text ?? null,
97+
customInstruction,
9498
},
9599
});
96100
lastResult = out;

0 commit comments

Comments
 (0)