运用AI翻译漫画(二)

发布时间:2024年01月05日

构建代码

构建这个PC桌面应用,我们需要几个步骤:

在得到第一次的显示结果后,经过测试,有很大可能会根据结果再对界面进行调整,实际上也是一个局部的软件工程中的迭代开发。

界面设计

启动Visual Studio 2017, 创建一个基于C#语言的WPF(Windows Presentation Foundation)项目:

WPF是一个非常成熟的技术,在有界面展示和交互的情况下,使用XAML设计/渲染引擎,比WinForm程序要强101倍,再加上有C#语言利器的帮助,是写PC桌面前端应用的最佳组合。

给Project起个名字,比如叫“CartoonTranslate”,选择最新的.NET Framework (4.6以上),然后点击”OK”。我们先一起来设计一下界面:

Input URL:用于输入互联网上的一张漫画图片的URL

Engine:指的是两个不同的算法引擎,其中,OCR旧引擎可以支持25种语言,识别效果可以接受;而Recognize Text新引擎目前只能支持英文,但效果比较好。

Language:制定当前要翻译的漫画的语言,我们只以英文和日文为例,其它国家的漫画相对较少,但一通百通,一样可以支持。

右侧的一堆Button了解一下:

Show:展示Input URL中的图片到下面的图片区

OCR:调用OCR服务

Translate:调用文本翻译服务,将日文或者英文翻译成中文

下侧大面积的图片区了解一下:

Source Image:原始漫画图片

Target Image:翻译成中文对白后的漫画图片

界面设计代码

我们在MainWindow.xaml文件里面填好以下code:

<Window x:Class="CartoonTranslate.MainWindow"
 ? ? ?  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 ? ? ?  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 ? ? ?  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 ? ? ?  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
 ? ? ?  xmlns:local="clr-namespace:CartoonTranslate"
 ? ? ?  mc:Ignorable="d"
 ? ? ?  Title="MainWindow" Height="450" Width="800">
 ?  <Grid>
 ? ? ?  <Grid.RowDefinitions>
 ? ? ? ? ?  <RowDefinition Height="Auto"/>
 ? ? ? ? ?  <RowDefinition Height="Auto"/>
 ? ? ? ? ?  <RowDefinition Height="Auto"/>
 ? ? ? ? ?  <RowDefinition Height="*"/>
 ? ? ?  </Grid.RowDefinitions>
 ? ? ?  <StackPanel Orientation="Horizontal" Grid.Row="0">
 ? ? ? ? ?  <TextBlock Grid.Row="0" Text="Input URL:"/>
 ? ? ? ? ?  <TextBox x:Name="tb_Url" Grid.Row="1" Width="600"
 ? ? ? ? ? ? ? ? ? ? Text="http://stat.ameba.jp/user_images/20121222/18/secretcube/2e/19/j/o0800112012341269548.jpg"/>
 ? ? ? ? ?  <Button x:Name="btn_Show" Content="Show" Click="btn_Show_Click" Width="100"/>
 ? ? ? ? ?  <Button x:Name="btn_OCR" Content="OCR" Click="btn_OCR_Click" Width="100"/>
 ? ? ? ? ?  <Button x:Name="btn_Translate" Content="Translate" Click="btn_Translate_Click" Width="100"/>
 ? ? ?  </StackPanel>
 ? ? ?  <StackPanel Grid.Row="1" Orientation="Horizontal">
 ? ? ? ? ?  <TextBlock Text="Engine:"/>
 ? ? ? ? ?  <RadioButton x:Name="rb_V1" GroupName="gn_Engine" Content="OCR" Margin="20,0" IsChecked="True" Click="rb_V1_Click"/>
 ? ? ? ? ?  <RadioButton x:Name="rb_V2" GroupName="gn_Engine" Content="Recognize Text" Click="rb_V2_Click"/>
 ? ? ? ? ?  <TextBlock Text="Language:" Margin="20,0"/>
 ? ? ? ? ?  <RadioButton x:Name="rb_English" GroupName="gn_Language" Content="English"/>
 ? ? ? ? ?  <RadioButton x:Name="rb_Japanese" GroupName="gn_Language" Content="Japanese" IsChecked="True" Margin="20,0"/>
 ? ? ?  </StackPanel>
 ? ? ?  <Grid Grid.Row="3">
 ? ? ? ? ?  <Grid.ColumnDefinitions>
 ? ? ? ? ? ? ?  <ColumnDefinition Width="*"/>
 ? ? ? ? ? ? ?  <ColumnDefinition Width="40"/>
 ? ? ? ? ? ? ?  <ColumnDefinition Width="*"/>
 ? ? ? ? ?  </Grid.ColumnDefinitions>
 ? ? ? ? ?  <TextBlock Grid.Column="0" Text="Source Image" VerticalAlignment="Center" HorizontalAlignment="Center"/>
 ? ? ? ? ?  <TextBlock Grid.Column="2" Text="Target Image" VerticalAlignment="Center" HorizontalAlignment="Center"/>
 ? ? ? ? ?  <Image x:Name="imgSource" Grid.Column="0" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
 ? ? ? ? ?  <Image x:Name="imgTarget" Grid.Column="2" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
 ? ? ? ? ?  <Canvas x:Name="canvas_1" Grid.Column="0"/>
 ? ? ? ? ?  <Canvas x:Name="canvas_2" Grid.Column="2"/>
 ? ? ?  </Grid>
</Grid>
</Window>

处理事件

关于XAML语法的问题不在本文的讨论范围之内。上面的XAML写好后,编译时会出错,因为里面定义了很多事件,在C#文件中还没有实现。所以,我们现在把事件代码补上。

局部变量定义(在MainWindow.xaml.cs的MainWindow class里面):

// using “OCR” or “Recognize Text”
private string Engine;
// source language, English or Japanese
private string Language;
// OCR result object
private OcrResult.Rootobject ocrResult;

按钮”Show”的事件

点击Show按钮的事件,把URL中的漫画的地址所指向的图片加载到窗口中显示:

private void btn_Show_Click(object sender, RoutedEventArgs e)
{
 ?  if (!Uri.IsWellFormedUriString(this.tb_Url.Text, UriKind.Absolute))
 ?  {
 ? ? ?  // show warning message
 ? ? ?  return;
 ?  }
 ?  // show image at imgSource
 ?  BitmapImage bi = new BitmapImage();
 ?  bi.BeginInit();
 ?  bi.UriSource = new Uri(this.tb_Url.Text);
 ?  bi.EndInit();
 ?  this.imgSource.Source = bi;
 ?  this.imgTarget.Source = bi;
}

在上面的代码中,同时给左右两个图片区域赋值,显示两张一样的图片。

按钮”OCR”的事件

点击OCR按钮的事件,会调用OCR REST API,然后根据返回结果把所有识别出来的文字用红色的矩形框标记上:

private async void btn_OCR_Click(object sender, RoutedEventArgs e)
{
 ?  this.Engine = GetEngine();
 ?  this.Language = GetLanguage();
 ?  if (Engine == "OCR")
 ?  {
 ? ? ?  ocrResult = await CognitiveServiceAgent.DoOCR(this.tb_Url.Text, Language);
 ? ? ?  foreach (OcrResult.Region region in ocrResult.regions)
 ? ? ?  {
 ? ? ? ? ?  foreach (OcrResult.Line line in region.lines)
 ? ? ? ? ?  {
 ? ? ? ? ? ? ?  if (line.Convert())
 ? ? ? ? ? ? ?  {
 ? ? ? ? ? ? ? ? ?  Rectangle rect = new Rectangle()
 ? ? ? ? ? ? ? ? ?  {
 ? ? ? ? ? ? ? ? ? ? ?  Margin = new Thickness(line.BB[0], line.BB[1], 0, 0),
 ? ? ? ? ? ? ? ? ? ? ?  Width = line.BB[2],
 ? ? ? ? ? ? ? ? ? ? ?  Height = line.BB[3],
 ? ? ? ? ? ? ? ? ? ? ?  Stroke = Brushes.Red,
 ? ? ? ? ? ? ? ? ? ? ?  //Fill =Brushes.White
 ? ? ? ? ? ? ? ? ?  };
 ? ? ? ? ? ? ? ? ?  this.canvas_1.Children.Add(rect);
 ? ? ? ? ? ? ?  }
 ? ? ? ? ?  }
 ? ? ?  }
 ?  }
 ?  else
 ?  {
 ?  }
}

在上面的代码中,通过调用DoOCR()自定义函数返回了反序列化好的类,再依次把返回结果集中的每个矩形生成一个Rectangle图形类,它的left和top用Margin的方式来定义,width和height直接赋值即可,把这些Rectangle图形类的实例添加到canvas_1的Visual Tree里即可显示出来(这个就是WPF的好处啦,不用处理绘图事件,但性能不如用Graphics类直接绘图)。

按钮”Translate”的事件

点击Translate按钮的事件:

private async void btn_Translate_Click(object sender, RoutedEventArgs e)
{
 ?  List<string> listTarget = await this.Translate();
 ?  this.ShowTargetText(listTarget);
}
?
private async Task<List<string>> Translate()
{
 ?  List<string> listSource = new List<string>();
 ?  List<string> listTarget = new List<string>();
 ?  if (this.Version == "OCR")
 ?  {
 ? ? ?  foreach (OcrResult.Region region in ocrResult.regions)
 ? ? ?  {
 ? ? ? ? ?  foreach (OcrResult.Line line in region.lines)
 ? ? ? ? ?  {
 ? ? ? ? ? ? ?  listSource.Add(line.TEXT);
 ? ? ? ? ? ? ?  if (listSource.Count >= 25)
 ? ? ? ? ? ? ?  {
 ? ? ? ? ? ? ? ? ?  List<string> listOutput = await CognitiveServiceAgent.DoTranslate(listSource, Language, "zh-Hans");
 ? ? ? ? ? ? ? ? ?  listTarget.AddRange(listOutput);
 ? ? ? ? ? ? ? ? ?  listSource.Clear();
 ? ? ? ? ? ? ?  }
 ? ? ? ? ?  }
 ? ? ?  }
 ? ? ?  if (listSource.Count > 0)
 ? ? ?  {
 ? ? ? ? ?  List<string> listOutput = await CognitiveServiceAgent.DoTranslate(listSource, Language, "zh-Hans");
 ? ? ? ? ?  listTarget.AddRange(listOutput);
 ? ? ?  }
 ?  }
 ?  return listTarget;
}
private void ShowTargetText(List<string> listTarget)
{
 ?  int i = 0;
 ?  foreach (OcrResult.Region region in ocrResult.regions)
 ?  {
 ? ? ?  foreach (OcrResult.Line line in region.lines)
 ? ? ?  {
 ? ? ? ? ?  string translatedLine = listTarget[i];
 ? ? ? ? ?  Rectangle rect = new Rectangle()
 ? ? ? ? ?  {
 ? ? ? ? ? ? ?  Margin = new Thickness(line.BB[0], line.BB[1], 0, 0),
 ? ? ? ? ? ? ?  Width = line.BB[2],
 ? ? ? ? ? ? ?  Height = line.BB[3],
 ? ? ? ? ? ? ?  Stroke = null,
 ? ? ? ? ? ? ?  Fill =Brushes.White
 ? ? ? ? ?  };
 ? ? ? ? ?  this.canvas_2.Children.Add(rect);
 ? ? ? ? ?  TextBlock tb = new TextBlock()
 ? ? ? ? ?  {
 ? ? ? ? ? ? ?  Margin = new Thickness(line.BB[0], line.BB[1], 0, 0),
 ? ? ? ? ? ? ?  Height = line.BB[3],
 ? ? ? ? ? ? ?  Width = line.BB[2],
 ? ? ? ? ? ? ?  Text = translatedLine,
 ? ? ? ? ? ? ?  FontSize = 16,
 ? ? ? ? ? ? ?  TextWrapping = TextWrapping.Wrap,
 ? ? ? ? ? ? ?  Foreground = Brushes.Red
 ? ? ? ? ?  };
 ? ? ? ? ?  this.canvas_2.Children.Add(tb);
 ? ? ? ? ?  i++;
 ? ? ?  }
 ?  }
}

上面这段代码中,包含了两个函数:this.Translate()和this.ShowTargetText()。

我们先看第一个函数:最难理解的地方可能是有个“25“数字,这是因为Translate API允许一次提交多个字符串并一起返回结果,这样比你提交25次字符串要快的多。翻译好的结果按顺序放在listOutput里,供后面使用。

再看第二个函数:先根据原始文字的矩形区域,生成一些白色的实心矩形,把它们贴在右侧的目标图片上,达到把原始文字覆盖(扣去)的目的。然后再根据每个原始矩形生成一个TextBlock,设定好它的位置和尺寸,再设置好翻译后的结果(translatedLine),这样就可以把中文文字贴到图上了。

选项按钮的事件

点击Radio Button的事件:

private void rb_V1_Click(object sender, RoutedEventArgs e)
{
 ?  this.rb_Japanese.IsEnabled = true;
}
private void rb_V2_Click(object sender, RoutedEventArgs e)
{
 ?  this.rb_English.IsChecked = true;
 ?  this.rb_Japanese.IsChecked = false;
 ?  this.rb_Japanese.IsEnabled = false;
}
private string GetLanguage()
{
 ?  if (this.rb_English.IsChecked == true)
 ?  {
 ? ? ?  return "en";
 ?  }
 ?  else
 ?  {
 ? ? ?  return "ja";
 ?  }
}
private string GetEngine()
{
 ?  if (this.rb_V1.IsChecked == true)
 ?  {
 ? ? ?  return "OCR";
 ?  }
 ?  else
 ?  {
 ? ? ?  return "RecText";
 ?  }
}

API数据访问部分

我们需要在CatroonTranslate工程中添加以下三个.cs文件:

  • CognitiveServiceAgent.cs

  • OcrResult.cs

  • TranslateResult.cs

与认知服务交互

CognitiveServiceAgent.cs文件完成与REST API交互的工作,包括调用OCR服务的和调用翻译服务的代码:

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Web;
?
namespace CartoonTranslate
{
 ?  class CognitiveServiceAgent
 ?  {
 ? ? ?  const string OcrEndPointV1 = "https://westcentralus.api.cognitive.microsoft.com/vision/v2.0/ocr?detectOrientation=true&language=";
 ? ? ?  const string OcrEndPointV2 = "https://westcentralus.api.cognitive.microsoft.com/vision/v2.0/recognizeText?mode=Printed";
 ? ? ?  const string VisionKey1 = "4c20ac56e1e7459a05e1497270022b";
 ? ? ?  const string VisionKey2 = "97992f0987e4be6b5be132309b8e57";
 ? ? ?  const string UrlContentTemplate = "{{\"url\":\"{0}\"}}";
?
 ? ? ?  const string TranslateEndPoint = "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&from={0}&to={1}";
 ? ? ?  const string TKey1 = "04023df3a4c499b1fc82510b48826c";
 ? ? ?  const string TKey2 = "9f76381748549cb503dae4a0d80a80";
?
 ? ? ?  public static async Task<List<string>> DoTranslate(List<string> text, string fromLanguage, string toLanguage)
 ? ? ?  {
 ? ? ? ? ?  try
 ? ? ? ? ?  {
 ? ? ? ? ? ? ?  using (HttpClient hc = new HttpClient())
 ? ? ? ? ? ? ?  {
 ? ? ? ? ? ? ? ? ?  hc.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", TKey1);
 ? ? ? ? ? ? ? ? ?  string jsonBody = CreateJsonBodyElement(text);
 ? ? ? ? ? ? ? ? ?  StringContent content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
 ? ? ? ? ? ? ? ? ?  string uri = string.Format(TranslateEndPoint, fromLanguage, toLanguage);
 ? ? ? ? ? ? ? ? ?  HttpResponseMessage resp = await hc.PostAsync(uri, content);
 ? ? ? ? ? ? ? ? ?  string json = await resp.Content.ReadAsStringAsync();
 ? ? ? ? ? ? ? ? ?  var ro = Newtonsoft.Json.JsonConvert.DeserializeObject<List<TranslateResult.Class1>>(json);
 ? ? ? ? ? ? ? ? ?  List<string> list = new List<string>();
 ? ? ? ? ? ? ? ? ?  foreach(TranslateResult.Class1 c in ro)
 ? ? ? ? ? ? ? ? ?  {
 ? ? ? ? ? ? ? ? ? ? ?  list.Add(c.translations[0].text);
 ? ? ? ? ? ? ? ? ?  }
 ? ? ? ? ? ? ? ? ?  return list;
 ? ? ? ? ? ? ?  }
 ? ? ? ? ?  }
 ? ? ? ? ?  catch (Exception ex)
 ? ? ? ? ?  {
 ? ? ? ? ? ? ?  Debug.WriteLine(ex.Message);
 ? ? ? ? ? ? ?  return null;
 ? ? ? ? ?  }
 ? ? ?  }
?
 ? ? ?  private static string CreateJsonBodyElement(List<string> text)
 ? ? ?  {
 ? ? ? ? ?  var a = text.Select(t => new { Text = t }).ToList();
 ? ? ? ? ?  var b = JsonConvert.SerializeObject(a);
 ? ? ? ? ?  return b;
 ? ? ?  }
?
 ? ? ?  /// <summary>
 ? ? ?  /// 
 ? ? ?  /// </summary>
 ? ? ?  /// <param name="imageUrl"></param>
 ? ? ?  /// <param name="language">en, ja, zh</param>
 ? ? ?  /// <returns></returns>
 ? ? ?  public static async Task<OcrResult.Rootobject> DoOCR(string imageUrl, string language)
 ? ? ?  {
 ? ? ? ? ?  try
 ? ? ? ? ?  {
 ? ? ? ? ? ? ?  using (HttpClient hc = new HttpClient())
 ? ? ? ? ? ? ?  {
 ? ? ? ? ? ? ? ? ?  ByteArrayContent content = CreateHeader(hc, imageUrl);
 ? ? ? ? ? ? ? ? ?  var uri = OcrEndPointV1 + language;
 ? ? ? ? ? ? ? ? ?  HttpResponseMessage resp = await hc.PostAsync(uri, content);
 ? ? ? ? ? ? ? ? ?  string result = string.Empty;
 ? ? ? ? ? ? ? ? ?  if (resp.StatusCode == System.Net.HttpStatusCode.OK)
 ? ? ? ? ? ? ? ? ?  {
 ? ? ? ? ? ? ? ? ? ? ?  string json = await resp.Content.ReadAsStringAsync();
 ? ? ? ? ? ? ? ? ? ? ?  Debug.WriteLine(json);
 ? ? ? ? ? ? ? ? ? ? ?  OcrResult.Rootobject ro = Newtonsoft.Json.JsonConvert.DeserializeObject<OcrResult.Rootobject>(json);
 ? ? ? ? ? ? ? ? ? ? ?  return ro;
 ? ? ? ? ? ? ? ? ?  }
 ? ? ? ? ? ? ?  }
 ? ? ? ? ? ? ?  return null;
 ? ? ? ? ?  }
 ? ? ? ? ?  catch (Exception ex)
 ? ? ? ? ?  {
 ? ? ? ? ? ? ?  Debug.Write(ex.Message);
 ? ? ? ? ? ? ?  return null;
 ? ? ? ? ?  }
 ? ? ?  }
?
 ? ? ?  private static ByteArrayContent CreateHeader(HttpClient hc, string imageUrl)
 ? ? ?  {
 ? ? ? ? ?  hc.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", VisionKey1);
 ? ? ? ? ?  string body = string.Format(UrlContentTemplate, imageUrl);
 ? ? ? ? ?  byte[] byteData = Encoding.UTF8.GetBytes(body);
 ? ? ? ? ?  var content = new ByteArrayContent(byteData);
 ? ? ? ? ?  content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
 ? ? ? ? ?  return content;
 ? ? ?  }
 ?  }
}

其中,DoTranslate()函数和DoOCR()函数都是HTTP调用,很容易理解。只有CreateJsonBodyElement函数需要解释一下。前面提到过我们一次允许给服务器提交25个字符串做批量翻译,因此传进来的是个List<string>,经过这个函数的简单处理,会得到以下JSON格式的数据作为HTTP的Body:

// JSON Data as Body
[
 ? ? ?  {“Text” : ”第1个字符串”},
 ? ? ?  {“Text” : ”第2个字符串”},
 ? ? ?  ……..
 ? ? ?  {“Text” : ”第25个字符串”},
]

OCR服务的数据类定义

OcrResult.cs文件是OCR服务返回的JSON数据所对应的类,用于反序列化:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
?
namespace CartoonTranslate.OcrResult
{
 ?  public class Rootobject
 ?  {
 ? ? ?  public string language { get; set; }
 ? ? ?  public string orientation { get; set; }
 ? ? ?  public float textAngle { get; set; }
 ? ? ?  public Region[] regions { get; set; }
 ?  }
?
 ?  public class Region
 ?  {
 ? ? ?  public string boundingBox { get; set; }
 ? ? ?  public Line[] lines { get; set; }
 ?  }
?
 ?  public class Line
 ?  {
 ? ? ?  public string boundingBox { get; set; }
 ? ? ?  public Word[] words { get; set; }
?
 ? ? ?  public int[] BB { get; set; }
 ? ? ?  public string TEXT { get; set; }
?
?
 ? ? ?  public bool Convert()
 ? ? ?  {
 ? ? ? ? ?  CombineWordToSentence();
 ? ? ? ? ?  return ConvertBBFromString2Int();
 ? ? ?  }
?
 ? ? ?  private bool ConvertBBFromString2Int()
 ? ? ?  {
 ? ? ? ? ?  string[] tmp = boundingBox.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
 ? ? ? ? ?  if (tmp.Length == 4)
 ? ? ? ? ?  {
 ? ? ? ? ? ? ?  BB = new int[4];
 ? ? ? ? ? ? ?  for (int i = 0; i < 4; i++)
 ? ? ? ? ? ? ?  {
 ? ? ? ? ? ? ? ? ?  int.TryParse(tmp[i], out BB[i]);
 ? ? ? ? ? ? ?  }
 ? ? ? ? ? ? ?  return true;
 ? ? ? ? ?  }
 ? ? ? ? ?  return false;
 ? ? ?  }
?
 ? ? ?  private void CombineWordToSentence()
 ? ? ?  {
 ? ? ? ? ?  StringBuilder sb = new StringBuilder();
 ? ? ? ? ?  foreach (Word word in words)
 ? ? ? ? ?  {
 ? ? ? ? ? ? ?  sb.Append(word.text);
 ? ? ? ? ?  }
 ? ? ? ? ?  this.TEXT = sb.ToString();
 ? ? ?  }
?
 ?  }
?
 ?  public class Word
 ?  {
 ? ? ?  public string boundingBox { get; set; }
 ? ? ?  public string text { get; set; }
 ?  }
}

需要说明的是,服务器返回的boundingBox是个string类型,在后面使用起来不太方便,需要把它转换成整数,所以增加了CovertBBFromString2Int()函数。还有就是返回的是一个个的词(Word),而不是一句话,所以增加了CombineWordToSentence()来把词连成句子。

翻译服务的数据类定义

TranslateResult.cs文件翻译服务返回的JSON所对应的类,用于反序列化:

namespace CartoonTranslate.TranslateResult
{
 ?  public class Class1
 ?  {
 ? ? ?  public Translation[] translations { get; set; }
 ?  }
?
 ?  public class Translation
 ?  {
 ? ? ?  public string text { get; set; }
 ? ? ?  public string to { get; set; }
 ?  }
}

小提示:在VS2017中,这种类不需要手工键入,可以在Debug模式下先把返回的JSON拷贝下来,然后新建一个.cs文件,在里面用Paste Special从JSON直接生成类就可以了。

文章来源:https://blog.csdn.net/2301_81887304/article/details/135401510
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。