PlotweaverModel commited on
Commit
b5db157
Β·
verified Β·
1 Parent(s): 4529152

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +475 -0
  2. requirements.txt +2 -0
app.py ADDED
@@ -0,0 +1,475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 🎬 Commentary Video Dubbing App β€” English to Arabic / German
3
+
4
+ """
5
+
6
+ import os
7
+ import base64
8
+ import shutil
9
+ import struct
10
+ import subprocess
11
+ import tempfile
12
+ import time
13
+
14
+ import gradio as gr
15
+ from openai import OpenAI
16
+
17
+ # ──────────────────────────────────────────────
18
+ # Configuration
19
+ # ──────────────────────────────────────────────
20
+ MODEL = "qwen3.5-omni-plus"
21
+ BASE_URL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
22
+
23
+ LANGUAGES = {
24
+ "Arabic (Ψ§Ω„ΨΉΨ±Ψ¨ΩŠΨ© الفءحى)": {
25
+ "code": "ar",
26
+ "system_prompt": (
27
+ "You are a professional video dubbing translator. You will receive audio in English.\n"
28
+ "Your task:\n"
29
+ "1. Listen carefully to the English speech.\n"
30
+ "2. Translate it into natural, fluent Modern Standard Arabic (Ψ§Ω„ΨΉΨ±Ψ¨ΩŠΨ© الفءحى).\n"
31
+ "3. Respond ONLY with the Arabic translation spoken aloud β€” no English, no commentary,\n"
32
+ " no meta-text, no transliteration. Speak entirely in Arabic.\n"
33
+ "4. Match the tone, emotion, and pacing of the original speaker as closely as possible.\n"
34
+ "5. If there are pauses or silence in the original audio, maintain similar pacing.\n"
35
+ "6. Translate idioms and cultural references into their Arabic equivalents.\n"
36
+ "7. Use clear, professional Arabic pronunciation suitable for a broad Arab audience."
37
+ ),
38
+ "user_prompt": "Translate this English speech into Arabic. Respond only with the spoken Arabic translation. Use Modern Standard Arabic (Ψ§Ω„ΨΉΨ±Ψ¨ΩŠΨ© الفءحى).",
39
+ },
40
+ "German (Deutsch)": {
41
+ "code": "de",
42
+ "system_prompt": (
43
+ "You are a professional video dubbing translator. You will receive audio in English.\n"
44
+ "Your task:\n"
45
+ "1. Listen carefully to the English speech.\n"
46
+ "2. Translate it into natural, fluent German.\n"
47
+ "3. Respond ONLY with the German translation spoken aloud β€” no English, no commentary,\n"
48
+ " no meta-text. Match the tone, emotion, and pacing of the original speaker as closely\n"
49
+ " as possible.\n"
50
+ "4. If there are pauses or silence in the original audio, maintain similar pacing.\n"
51
+ "5. Translate idioms and cultural references into their German equivalents rather than\n"
52
+ " translating literally."
53
+ ),
54
+ "user_prompt": "Translate this English speech into German. Respond only with the spoken German translation.",
55
+ },
56
+ "French (FranΓ§ais)": {
57
+ "code": "fr",
58
+ "system_prompt": (
59
+ "You are a professional video dubbing translator. You will receive audio in English.\n"
60
+ "Your task:\n"
61
+ "1. Listen carefully to the English speech.\n"
62
+ "2. Translate it into natural, fluent French.\n"
63
+ "3. Respond ONLY with the French translation spoken aloud β€” no English, no commentary,\n"
64
+ " no meta-text. Match the tone, emotion, and pacing of the original speaker.\n"
65
+ "4. If there are pauses or silence in the original audio, maintain similar pacing.\n"
66
+ "5. Translate idioms and cultural references into their French equivalents."
67
+ ),
68
+ "user_prompt": "Translate this English speech into French. Respond only with the spoken French translation.",
69
+ },
70
+ "Spanish (EspaΓ±ol)": {
71
+ "code": "es",
72
+ "system_prompt": (
73
+ "You are a professional video dubbing translator. You will receive audio in English.\n"
74
+ "Your task:\n"
75
+ "1. Listen carefully to the English speech.\n"
76
+ "2. Translate it into natural, fluent Spanish.\n"
77
+ "3. Respond ONLY with the Spanish translation spoken aloud β€” no English, no commentary,\n"
78
+ " no meta-text. Match the tone, emotion, and pacing of the original speaker.\n"
79
+ "4. If there are pauses or silence in the original audio, maintain similar pacing.\n"
80
+ "5. Translate idioms and cultural references into their Spanish equivalents."
81
+ ),
82
+ "user_prompt": "Translate this English speech into Spanish. Respond only with the spoken Spanish translation.",
83
+ },
84
+ "Russian (Русский)": {
85
+ "code": "ru",
86
+ "system_prompt": (
87
+ "You are a professional video dubbing translator. You will receive audio in English.\n"
88
+ "Your task:\n"
89
+ "1. Listen carefully to the English speech.\n"
90
+ "2. Translate it into natural, fluent Russian.\n"
91
+ "3. Respond ONLY with the Russian translation spoken aloud β€” no English, no commentary,\n"
92
+ " no meta-text. Match the tone, emotion, and pacing of the original speaker.\n"
93
+ "4. If there are pauses or silence in the original audio, maintain similar pacing.\n"
94
+ "5. Translate idioms and cultural references into their Russian equivalents."
95
+ ),
96
+ "user_prompt": "Translate this English speech into Russian. Respond only with the spoken Russian translation.",
97
+ },
98
+ "Japanese (ζ—₯本θͺž)": {
99
+ "code": "ja",
100
+ "system_prompt": (
101
+ "You are a professional video dubbing translator. You will receive audio in English.\n"
102
+ "Your task:\n"
103
+ "1. Listen carefully to the English speech.\n"
104
+ "2. Translate it into natural, fluent Japanese.\n"
105
+ "3. Respond ONLY with the Japanese translation spoken aloud β€” no English, no commentary,\n"
106
+ " no meta-text. Match the tone, emotion, and pacing of the original speaker.\n"
107
+ "4. If there are pauses or silence in the original audio, maintain similar pacing.\n"
108
+ "5. Translate idioms and cultural references into their Japanese equivalents."
109
+ ),
110
+ "user_prompt": "Translate this English speech into Japanese. Respond only with the spoken Japanese translation.",
111
+ },
112
+ "Korean (ν•œκ΅­μ–΄)": {
113
+ "code": "ko",
114
+ "system_prompt": (
115
+ "You are a professional video dubbing translator. You will receive audio in English.\n"
116
+ "Your task:\n"
117
+ "1. Listen carefully to the English speech.\n"
118
+ "2. Translate it into natural, fluent Korean.\n"
119
+ "3. Respond ONLY with the Korean translation spoken aloud β€” no English, no commentary,\n"
120
+ " no meta-text. Match the tone, emotion, and pacing of the original speaker.\n"
121
+ "4. If there are pauses or silence in the original audio, maintain similar pacing.\n"
122
+ "5. Translate idioms and cultural references into their Korean equivalents."
123
+ ),
124
+ "user_prompt": "Translate this English speech into Korean. Respond only with the spoken Korean translation.",
125
+ },
126
+ "Portuguese (PortuguΓͺs)": {
127
+ "code": "pt",
128
+ "system_prompt": (
129
+ "You are a professional video dubbing translator. You will receive audio in English.\n"
130
+ "Your task:\n"
131
+ "1. Listen carefully to the English speech.\n"
132
+ "2. Translate it into natural, fluent Portuguese.\n"
133
+ "3. Respond ONLY with the Portuguese translation spoken aloud β€” no English, no commentary,\n"
134
+ " no meta-text. Match the tone, emotion, and pacing of the original speaker.\n"
135
+ "4. If there are pauses or silence in the original audio, maintain similar pacing.\n"
136
+ "5. Translate idioms and cultural references into their Portuguese equivalents."
137
+ ),
138
+ "user_prompt": "Translate this English speech into Portuguese. Respond only with the spoken Portuguese translation.",
139
+ },
140
+ "Italian (Italiano)": {
141
+ "code": "it",
142
+ "system_prompt": (
143
+ "You are a professional video dubbing translator. You will receive audio in English.\n"
144
+ "Your task:\n"
145
+ "1. Listen carefully to the English speech.\n"
146
+ "2. Translate it into natural, fluent Italian.\n"
147
+ "3. Respond ONLY with the Italian translation spoken aloud β€” no English, no commentary,\n"
148
+ " no meta-text. Match the tone, emotion, and pacing of the original speaker.\n"
149
+ "4. If there are pauses or silence in the original audio, maintain similar pacing.\n"
150
+ "5. Translate idioms and cultural references into their Italian equivalents."
151
+ ),
152
+ "user_prompt": "Translate this English speech into Italian. Respond only with the spoken Italian translation.",
153
+ },
154
+ "Chinese (δΈ­ζ–‡)": {
155
+ "code": "zh",
156
+ "system_prompt": (
157
+ "You are a professional video dubbing translator. You will receive audio in English.\n"
158
+ "Your task:\n"
159
+ "1. Listen carefully to the English speech.\n"
160
+ "2. Translate it into natural, fluent Mandarin Chinese.\n"
161
+ "3. Respond ONLY with the Chinese translation spoken aloud β€” no English, no commentary,\n"
162
+ " no meta-text. Match the tone, emotion, and pacing of the original speaker.\n"
163
+ "4. If there are pauses or silence in the original audio, maintain similar pacing.\n"
164
+ "5. Translate idioms and cultural references into their Chinese equivalents."
165
+ ),
166
+ "user_prompt": "Translate this English speech into Mandarin Chinese. Respond only with the spoken Chinese translation.",
167
+ },
168
+ }
169
+
170
+ VOICES = [
171
+ "Cherry", "Serena", "Ethan", "Chelsie", "Momo", "Vivian", "Moon", "Maia",
172
+ "Kai", "Nofish", "Bella", "Jennifer", "Ryan", "Katerina", "Aiden",
173
+ "Eldric Sage", "Mia", "Mochi", "Bellona", "Vincent", "Bunny", "Neil",
174
+ "Elias", "Arthur", "Seren", "Bodega", "Sonrisa", "Alek", "Dolce",
175
+ "Sohee", "Ono Anna", "Lenn", "Emilien", "Andre",
176
+ ]
177
+
178
+ # ──────────────────────────────────────────────
179
+ # Audio helpers
180
+ # ──────────────────────────────────────────────
181
+ def get_duration(filepath: str) -> float:
182
+ result = subprocess.run(
183
+ ["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
184
+ "-of", "default=noprint_wrappers=1:nokey=1", filepath],
185
+ capture_output=True, text=True,
186
+ )
187
+ return float(result.stdout.strip())
188
+
189
+
190
+ def extract_audio_chunk(video_path, output_wav, start_sec, duration_sec):
191
+ subprocess.run(
192
+ ["ffmpeg", "-y", "-ss", str(start_sec), "-t", str(duration_sec),
193
+ "-i", video_path, "-vn", "-acodec", "pcm_s16le",
194
+ "-ar", "16000", "-ac", "1", output_wav],
195
+ capture_output=True, check=True,
196
+ )
197
+
198
+
199
+ def wav_to_base64(wav_path):
200
+ with open(wav_path, "rb") as f:
201
+ return base64.b64encode(f.read()).decode("utf-8")
202
+
203
+
204
+ def base64_to_wav(b64_data, output_path):
205
+ audio_bytes = base64.b64decode(b64_data)
206
+ sample_rate = 24000
207
+ num_channels = 1
208
+ bits_per_sample = 16
209
+ byte_rate = sample_rate * num_channels * bits_per_sample // 8
210
+ block_align = num_channels * bits_per_sample // 8
211
+ data_size = len(audio_bytes)
212
+ with open(output_path, "wb") as f:
213
+ f.write(b"RIFF")
214
+ f.write(struct.pack("<I", 36 + data_size))
215
+ f.write(b"WAVE")
216
+ f.write(b"fmt ")
217
+ f.write(struct.pack("<I", 16))
218
+ f.write(struct.pack("<H", 1))
219
+ f.write(struct.pack("<H", num_channels))
220
+ f.write(struct.pack("<I", sample_rate))
221
+ f.write(struct.pack("<I", byte_rate))
222
+ f.write(struct.pack("<H", block_align))
223
+ f.write(struct.pack("<H", bits_per_sample))
224
+ f.write(b"data")
225
+ f.write(struct.pack("<I", data_size))
226
+ f.write(audio_bytes)
227
+
228
+
229
+ def concatenate_wavs(wav_files, output_path):
230
+ if len(wav_files) == 1:
231
+ shutil.copy2(wav_files[0], output_path)
232
+ return
233
+ list_file = output_path + ".txt"
234
+ with open(list_file, "w") as f:
235
+ for wav in wav_files:
236
+ f.write(f"file '{wav}'\n")
237
+ subprocess.run(
238
+ ["ffmpeg", "-y", "-f", "concat", "-safe", "0",
239
+ "-i", list_file, "-c", "copy", output_path],
240
+ capture_output=True, check=True,
241
+ )
242
+ os.remove(list_file)
243
+
244
+
245
+ def mux_audio_to_video(original_video, new_audio, output_video):
246
+ result = subprocess.run(
247
+ ["ffmpeg", "-y", "-i", original_video, "-i", new_audio,
248
+ "-c:v", "copy", "-map", "0:v:0", "-map", "1:a:0",
249
+ "-shortest", output_video],
250
+ capture_output=True, text=True,
251
+ )
252
+ if result.returncode != 0:
253
+ raise RuntimeError(f"FFmpeg mux failed:\n{result.stderr[-500:]}")
254
+
255
+
256
+ # ──────────────────────────────────────────────
257
+ # Translation
258
+ # ──────────────────────────────────────────────
259
+ def translate_chunk(client, wav_path, voice, lang_config, chunk_index):
260
+ audio_b64 = wav_to_base64(wav_path)
261
+ output_wav = wav_path.replace(".wav", f"_{lang_config['code']}.wav")
262
+
263
+ completion = client.chat.completions.create(
264
+ model=MODEL,
265
+ messages=[
266
+ {"role": "system", "content": lang_config["system_prompt"]},
267
+ {
268
+ "role": "user",
269
+ "content": [
270
+ {
271
+ "type": "input_audio",
272
+ "input_audio": {
273
+ "data": f"data:audio/wav;base64,{audio_b64}",
274
+ "format": "wav",
275
+ },
276
+ },
277
+ {"type": "text", "text": lang_config["user_prompt"]},
278
+ ],
279
+ },
280
+ ],
281
+ modalities=["text", "audio"],
282
+ audio={"voice": voice, "format": "wav"},
283
+ stream=True,
284
+ stream_options={"include_usage": True},
285
+ )
286
+
287
+ audio_chunks = []
288
+ transcript_parts = []
289
+
290
+ for event in completion:
291
+ if not event.choices:
292
+ continue
293
+ delta = event.choices[0].delta
294
+ if hasattr(delta, "content") and delta.content:
295
+ transcript_parts.append(delta.content)
296
+ if hasattr(delta, "audio") and delta.audio:
297
+ if isinstance(delta.audio, dict):
298
+ if "data" in delta.audio:
299
+ audio_chunks.append(delta.audio["data"])
300
+ elif hasattr(delta.audio, "data") and delta.audio.data:
301
+ audio_chunks.append(delta.audio.data)
302
+
303
+ transcript = "".join(transcript_parts)
304
+
305
+ if audio_chunks:
306
+ full_audio_b64 = "".join(audio_chunks)
307
+ base64_to_wav(full_audio_b64, output_wav)
308
+ return output_wav, transcript
309
+ return None, transcript
310
+
311
+
312
+ # ──────────────────────────────────────────────
313
+ # Main pipeline (called by Gradio)
314
+ # ──────────────────────────────────────────────
315
+ def dub_video(video_file, target_language, voice, chunk_seconds, progress=gr.Progress()):
316
+ if video_file is None:
317
+ raise gr.Error("Please upload a video file.")
318
+
319
+ api_key = os.environ.get("DASHSCOPE_API_KEY", "")
320
+ if not api_key:
321
+ raise gr.Error(
322
+ "DASHSCOPE_API_KEY not set. Add it as a Space Secret "
323
+ "(Settings β†’ Secrets β†’ New Secret)."
324
+ )
325
+
326
+ lang_config = LANGUAGES[target_language]
327
+ client = OpenAI(api_key=api_key, base_url=BASE_URL)
328
+ tmp_dir = tempfile.mkdtemp(prefix="dub_")
329
+
330
+ try:
331
+ # ── Duration ──
332
+ progress(0.05, desc="Analyzing video...")
333
+ total_duration = get_duration(video_file)
334
+
335
+ if total_duration > 3600:
336
+ raise gr.Error("Video is longer than 1 hour. Please use a shorter clip.")
337
+
338
+ # ── Split ──
339
+ progress(0.1, desc="Extracting audio chunks...")
340
+ num_chunks = max(
341
+ 1,
342
+ int(total_duration // chunk_seconds)
343
+ + (1 if total_duration % chunk_seconds > 0 else 0),
344
+ )
345
+
346
+ input_chunks = []
347
+ for i in range(num_chunks):
348
+ start = i * chunk_seconds
349
+ duration = min(chunk_seconds, total_duration - start)
350
+ chunk_path = os.path.join(tmp_dir, f"chunk_{i:03d}.wav")
351
+ extract_audio_chunk(video_file, chunk_path, start, duration)
352
+ input_chunks.append(chunk_path)
353
+
354
+ # ── Translate ──
355
+ output_chunks = []
356
+ all_transcripts = []
357
+
358
+ for i, chunk_path in enumerate(input_chunks):
359
+ frac = 0.15 + 0.7 * (i / num_chunks)
360
+ progress(frac, desc=f"Translating chunk {i+1}/{num_chunks}...")
361
+
362
+ result_path, transcript = translate_chunk(
363
+ client, chunk_path, voice, lang_config, i
364
+ )
365
+ if transcript:
366
+ all_transcripts.append(transcript)
367
+
368
+ if result_path:
369
+ output_chunks.append(result_path)
370
+ else:
371
+ # Silence fallback
372
+ duration = get_duration(chunk_path)
373
+ silence_path = os.path.join(tmp_dir, f"silence_{i:03d}.wav")
374
+ subprocess.run(
375
+ ["ffmpeg", "-y", "-f", "lavfi",
376
+ "-i", "anullsrc=r=24000:cl=mono",
377
+ "-t", str(duration), "-acodec", "pcm_s16le", silence_path],
378
+ capture_output=True, check=True,
379
+ )
380
+ output_chunks.append(silence_path)
381
+
382
+ # ── Concatenate ──
383
+ progress(0.88, desc="Assembling audio...")
384
+ full_audio = os.path.join(tmp_dir, "full_dubbed_audio.wav")
385
+ concatenate_wavs(output_chunks, full_audio)
386
+
387
+ # ── Mux ──
388
+ progress(0.93, desc="Muxing audio onto video...")
389
+ ext = os.path.splitext(video_file)[1] or ".mp4"
390
+ output_video = os.path.join(tmp_dir, f"dubbed_{lang_config['code']}{ext}")
391
+ mux_audio_to_video(video_file, full_audio, output_video)
392
+
393
+ progress(1.0, desc="Done!")
394
+
395
+ transcript_text = "\n\n".join(
396
+ f"**Chunk {i+1}:**\n{t}" for i, t in enumerate(all_transcripts)
397
+ ) or "No transcript available."
398
+
399
+ return output_video, transcript_text
400
+
401
+ except Exception as e:
402
+ # Clean up on error
403
+ shutil.rmtree(tmp_dir, ignore_errors=True)
404
+ raise gr.Error(str(e))
405
+
406
+
407
+ # ──────────────────────────────────────────────
408
+ # Gradio UI
409
+ # ──────────────────────────────────────────────
410
+ DESCRIPTION = """
411
+ # 🎬 Commentary Video Dubbing β€” English to Any Language
412
+
413
+ Upload an English video and get it dubbed into Arabic, German, French, Spanish, and more.
414
+ The model translates the speech and generates natural-sounding voice output in the target language.
415
+
416
+ **Supported output languages:** Arabic, Chinese, German, French, Spanish, Portuguese, Italian, Russian, Japanese, Korean
417
+
418
+ """
419
+
420
+ with gr.Blocks(
421
+ title="Video Dubbing β€” Qwen3.5-Omni",
422
+ theme=gr.themes.Soft(
423
+ primary_hue="amber",
424
+ secondary_hue="orange",
425
+ neutral_hue="stone",
426
+ ),
427
+ ) as demo:
428
+
429
+ gr.Markdown(DESCRIPTION)
430
+
431
+ with gr.Row():
432
+ with gr.Column(scale=1):
433
+ video_input = gr.Video(label="Upload English Video", sources=["upload"])
434
+
435
+ target_lang = gr.Dropdown(
436
+ choices=list(LANGUAGES.keys()),
437
+ value="Arabic (Ψ§Ω„ΨΉΨ±Ψ¨ΩŠΨ© الفءحى)",
438
+ label="Target Language",
439
+ )
440
+
441
+ voice_select = gr.Dropdown(
442
+ choices=VOICES,
443
+ value="Ethan",
444
+ label="Voice",
445
+ info="All voices support all output languages.",
446
+ )
447
+
448
+ chunk_slider = gr.Slider(
449
+ minimum=30,
450
+ maximum=300,
451
+ value=120,
452
+ step=10,
453
+ label="Chunk Duration (seconds)",
454
+ info="Shorter chunks = more API calls but less risk of timeout.",
455
+ )
456
+
457
+ dub_btn = gr.Button("πŸŽ™οΈ Start Dubbing", variant="primary", size="lg")
458
+
459
+ with gr.Column(scale=1):
460
+ video_output = gr.Video(label="Dubbed Video")
461
+ transcript_output = gr.Markdown(label="Translation Transcript")
462
+
463
+ dub_btn.click(
464
+ fn=dub_video,
465
+ inputs=[video_input, target_lang, voice_select, chunk_slider],
466
+ outputs=[video_output, transcript_output],
467
+ )
468
+
469
+ gr.Markdown(
470
+ "---\n"
471
+ "**Built by:** Plotweaver "
472
+ )
473
+
474
+ if __name__ == "__main__":
475
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ openai>=1.52.0
2
+ gradio>=5.0.0