Tes / app.py
don0726's picture
Update app.py
5caf4d9 verified
Raw
History Blame Contribute Delete
8.22 kB
import gradio as gr
import json
import anthropic
client = anthropic.Anthropic()
LANGUAGES = [
"Hindi", "Arabic", "French", "German", "Spanish", "Chinese",
"Japanese", "Russian", "Urdu", "Turkish", "Korean", "Italian",
"Portuguese", "English",
]
GAP_MS = 200 # ms gap between words β†’ new line
MAX_SPEED = 2.5 # maximum TTS speed multiplier
MAX_CHARS_RATIO = MAX_SPEED # at 2.5x speed, can fit 2.5x characters
def group_words_into_lines(words: list, gap_ms: float = GAP_MS) -> list:
"""
Group word-level timestamps into subtitle lines.
A new line starts whenever the gap between two consecutive words
exceeds gap_ms milliseconds.
Each word dict must have: {"start": float, "end": float, "word": str}
Returns list of line dicts:
{"start": float, "end": float, "text": str, "words": [...]}
"""
if not words:
return []
lines = []
current_words = [words[0]]
for w in words[1:]:
prev_end = current_words[-1]["end"]
gap_secs = w["start"] - prev_end
gap_ms_val = gap_secs * 1000
if gap_ms_val > gap_ms:
# flush current line
lines.append({
"start": current_words[0]["start"],
"end": current_words[-1]["end"],
"text": " ".join(cw["word"].strip() for cw in current_words),
"words": current_words,
})
current_words = [w]
else:
current_words.append(w)
# flush last line
if current_words:
lines.append({
"start": current_words[0]["start"],
"end": current_words[-1]["end"],
"text": " ".join(cw["word"].strip() for cw in current_words),
"words": current_words,
})
return lines
def fmt_time(s: float) -> str:
m = int(s) // 60
sec = s - m * 60
return f"{m:02d}:{sec:05.2f}"
def translate_line(text: str, src_lang: str, tgt_lang: str,
max_chars: int) -> str:
"""
Translate one line via Claude, instructing it to stay under max_chars
while NEVER cutting content β€” just use natural concise phrasing.
"""
prompt = (
f"Translate the following {src_lang} text to {tgt_lang}.\n"
f"Rules:\n"
f"1. Never cut or omit any meaning or content.\n"
f"2. Try to keep the translation under {max_chars} characters "
f"by using natural, concise phrasing in {tgt_lang}.\n"
f"3. If it is impossible to stay under {max_chars} characters "
f"without cutting content, go slightly over β€” completeness wins.\n"
f"4. Output ONLY the translated text, nothing else.\n\n"
f"Text: {text}"
)
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1000,
messages=[{"role": "user", "content": prompt}],
)
return message.content[0].text.strip()
def process(words_json: str, src_lang: str, tgt_lang: str,
max_speed: float, gap_ms: float) -> tuple:
"""
Main processing function.
Returns (display_text, json_output).
"""
if not words_json.strip():
return "❌ Please paste word-level JSON.", ""
try:
data = json.loads(words_json)
except json.JSONDecodeError as e:
return f"❌ Invalid JSON: {e}", ""
# Accept both {"words": [...]} and bare [...]
if isinstance(data, dict):
words = data.get("words", [])
elif isinstance(data, list):
words = data
else:
return "❌ JSON must be a list of words or {\"words\": [...]}", ""
if not words:
return "❌ No words found in JSON.", ""
# Validate first word has required fields
required = {"start", "end", "word"}
if not required.issubset(words[0].keys()):
return f"❌ Each word must have: {required}", ""
lines = group_words_into_lines(words, gap_ms_override := gap_ms)
results = []
display = []
warnings = []
for i, line in enumerate(lines):
src_text = line["text"]
src_chars = len(src_text)
max_chars = int(src_chars * max_speed)
# Translate
tgt_text = translate_line(src_text, src_lang, tgt_lang, max_chars)
tgt_chars = len(tgt_text)
# Speed factor needed to fit translation in the same time slot
# (proportional to character count ratio)
ratio = tgt_chars / max(src_chars, 1)
required_speed = round(ratio, 3)
over_limit = required_speed > max_speed
slot_start = line["start"]
slot_end = line["end"]
result = {
"line": i + 1,
"start": round(slot_start, 3),
"end": round(slot_end, 3),
"original": src_text,
"translated": tgt_text,
"original_chars": src_chars,
"translated_chars": tgt_chars,
"char_ratio": round(ratio, 3),
"required_speed": required_speed,
"max_speed": max_speed,
"over_limit": over_limit,
}
results.append(result)
# Human-readable display line
flag = "⚠️ " if over_limit else "βœ… "
display.append(
f"{flag}[{fmt_time(slot_start)} β†’ {fmt_time(slot_end)}]\n"
f" {src_lang}: {src_text}\n"
f" {tgt_lang}: {tgt_text}\n"
f" chars: {src_chars} β†’ {tgt_chars} "
f"speed: {required_speed}x"
+ (" ⚠️ OVER {:.1f}x LIMIT".format(max_speed) if over_limit else "")
)
if over_limit:
warnings.append(
f"Line {i+1}: needs {required_speed}x but limit is {max_speed}x"
)
summary = (
f"βœ… {len(lines)} lines processed. "
f"{'⚠️ ' + str(len(warnings)) + ' lines over speed limit.' if warnings else 'All within speed limit.'}"
)
display_text = summary + "\n\n" + "\n\n".join(display)
json_out = json.dumps(results, ensure_ascii=False, indent=2)
return display_text, json_out
with gr.Blocks(title="Dubbing Line Builder") as demo:
gr.Markdown("""
# 🎬 Dubbing Line Builder
Paste word-level timestamps β†’ groups into lines (200ms gap rule) β†’
translates β†’ calculates required TTS speed per line.
**Input JSON format** (from your Whisper API):
```json
[{"start": 0.5, "end": 0.9, "word": "Hello"},
{"start": 1.0, "end": 1.4, "word": "world"}]
```
or `{"words": [...]}` shape also accepted.
""")
with gr.Row():
with gr.Column(scale=1):
words_input = gr.Textbox(
label="πŸ“‹ Word-level JSON",
lines=15,
placeholder='[{"start": 0.5, "end": 0.9, "word": "Hello"}, ...]'
)
with gr.Row():
src_lang = gr.Dropdown(
label="πŸ“’ Source language",
choices=LANGUAGES, value="English"
)
tgt_lang = gr.Dropdown(
label="🎯 Target language",
choices=LANGUAGES, value="Hindi"
)
with gr.Row():
max_speed_input = gr.Slider(
label="⚑ Max TTS speed (x)",
minimum=1.0, maximum=3.0, step=0.1, value=2.5,
info="Max chars = original Γ— this value"
)
gap_input = gr.Slider(
label="⏱ Gap rule (ms)",
minimum=50, maximum=1000, step=50, value=200,
info="New line if silence > this many ms"
)
btn = gr.Button("πŸš€ Build Lines & Translate", variant="primary")
with gr.Column(scale=1):
display_out = gr.Textbox(label="πŸ“ Result", lines=25)
json_out = gr.Textbox(label="πŸ“¦ JSON output (for your app)", lines=15)
btn.click(
fn=process,
inputs=[words_input, src_lang, tgt_lang, max_speed_input, gap_input],
outputs=[display_out, json_out],
)
gr.api(process, api_name="build_lines")
if __name__ == "__main__":
demo.launch()