Initial commit
This commit is contained in:
commit
bc77006352
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
||||
## 1.0.0
|
||||
|
||||
- Initial version.
|
39
README.md
Normal file
39
README.md
Normal file
@ -0,0 +1,39 @@
|
||||
<!--
|
||||
This README describes the package. If you publish this package to pub.dev,
|
||||
this README's contents appear on the landing page for your package.
|
||||
|
||||
For information about how to write a good package README, see the guide for
|
||||
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
||||
|
||||
For general information about developing packages, see the Dart guide for
|
||||
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
|
||||
and the Flutter guide for
|
||||
[developing packages and plugins](https://flutter.dev/developing-packages).
|
||||
-->
|
||||
|
||||
TODO: Put a short description of the package here that helps potential users
|
||||
know whether this package might be useful for them.
|
||||
|
||||
## Features
|
||||
|
||||
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||
|
||||
## Getting started
|
||||
|
||||
TODO: List prerequisites and provide or point to information on how to
|
||||
start using the package.
|
||||
|
||||
## Usage
|
||||
|
||||
TODO: Include short and useful examples for package users. Add longer examples
|
||||
to `/example` folder.
|
||||
|
||||
```dart
|
||||
const like = 'sample';
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
||||
TODO: Tell users more about the package: where to find more information, how to
|
||||
contribute to the package, how to file issues, what response they can expect
|
||||
from the package authors, and more.
|
30
analysis_options.yaml
Normal file
30
analysis_options.yaml
Normal file
@ -0,0 +1,30 @@
|
||||
# This file configures the static analysis results for your project (errors,
|
||||
# warnings, and lints).
|
||||
#
|
||||
# This enables the 'recommended' set of lints from `package:lints`.
|
||||
# This set helps identify many issues that may lead to problems when running
|
||||
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||
# style and format.
|
||||
#
|
||||
# If you want a smaller set of lints you can change this to specify
|
||||
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||
# (the recommended set includes the core lints).
|
||||
# The core lints are also what is used by pub.dev for scoring packages.
|
||||
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
# Uncomment the following section to specify additional rules.
|
||||
|
||||
# linter:
|
||||
# rules:
|
||||
# - camel_case_types
|
||||
|
||||
# analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
|
||||
# For more information about the core and recommended set of lints, see
|
||||
# https://dart.dev/go/core-lints
|
||||
|
||||
# For additional information about configuring this file, see
|
||||
# https://dart.dev/guides/language/analysis-options
|
15
example/perspective_api_example.dart
Normal file
15
example/perspective_api_example.dart
Normal file
@ -0,0 +1,15 @@
|
||||
import 'package:perspective_api/perspective_api.dart';
|
||||
|
||||
void main(List<String> args) async {
|
||||
final client = PerspectiveApi(
|
||||
apiKey: 'AIzaSyBbovs3G9iJBycX_K_JmfXDkoi0lRj-ifA',
|
||||
);
|
||||
|
||||
final response = await client.analyzeComment(
|
||||
'Salut, belle gosse 😏',
|
||||
requestedAttributes: {RequestedAttribute.flirtation},
|
||||
languages: {Language.french},
|
||||
);
|
||||
|
||||
print(response);
|
||||
}
|
7
lib/perspective_api.dart
Normal file
7
lib/perspective_api.dart
Normal file
@ -0,0 +1,7 @@
|
||||
library perspective_api;
|
||||
|
||||
export 'src/perspective_api.dart';
|
||||
export 'src/models/analyze_comment_response.dart';
|
||||
export 'src/models/language.dart';
|
||||
export 'src/models/requested_attribute.dart';
|
||||
export 'src/models/attribute_score.dart';
|
71
lib/src/http/request.dart
Normal file
71
lib/src/http/request.dart
Normal file
@ -0,0 +1,71 @@
|
||||
import 'package:http/http.dart';
|
||||
|
||||
/// An HTTP request to be made against the API.
|
||||
abstract class HttpRequest {
|
||||
/// The route for this request.
|
||||
final String route;
|
||||
|
||||
/// The method for this request.
|
||||
final String method;
|
||||
|
||||
/// The query parameters for this request.
|
||||
final Map<String, String> queryParameters;
|
||||
|
||||
/// The headers for this request.
|
||||
final Map<String, String> headers;
|
||||
|
||||
/// Create a new [HttpRequest].
|
||||
///
|
||||
/// {@macro http_request}
|
||||
HttpRequest(
|
||||
this.route, {
|
||||
this.method = 'GET',
|
||||
this.queryParameters = const {},
|
||||
this.headers = const {},
|
||||
});
|
||||
|
||||
/// Transform this [HttpRequest] into a [BaseRequest] to be sent.
|
||||
BaseRequest prepare();
|
||||
|
||||
Uri _getUri() => Uri.https(
|
||||
'commentanalyzer.googleapis.com',
|
||||
'/v1alpha1/$route',
|
||||
queryParameters.isNotEmpty ? queryParameters : null,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => 'HttpRequest($method $route)';
|
||||
}
|
||||
|
||||
/// An [HttpRequest] with a JSON body.
|
||||
class BasicRequest extends HttpRequest {
|
||||
/// The `Content-Type` header for JSON requests.
|
||||
static const jsonContentTypeHeader = {'Content-Type': 'application/json'};
|
||||
|
||||
/// The JSON-encoded body of this request.
|
||||
///
|
||||
/// Set to `null` to send no body.
|
||||
final String? body;
|
||||
|
||||
/// Create a new [BasicRequest].
|
||||
BasicRequest(
|
||||
super.route, {
|
||||
this.body,
|
||||
super.method,
|
||||
super.queryParameters,
|
||||
super.headers,
|
||||
});
|
||||
|
||||
@override
|
||||
Request prepare() {
|
||||
final request = Request(method, _getUri());
|
||||
request.headers.addAll(headers);
|
||||
|
||||
if (body != null) {
|
||||
request.headers.addAll(jsonContentTypeHeader);
|
||||
request.body = body!;
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
50
lib/src/models/analyze_comment_response.dart
Normal file
50
lib/src/models/analyze_comment_response.dart
Normal file
@ -0,0 +1,50 @@
|
||||
import 'package:perspective_api/src/models/attribute_score.dart';
|
||||
import 'package:perspective_api/src/models/language.dart';
|
||||
|
||||
import 'requested_attribute.dart';
|
||||
|
||||
class AnalyzeCommentResponse {
|
||||
/// The requested languages.
|
||||
final Set<Language> languages;
|
||||
|
||||
/// The auto-detected languages.
|
||||
final Set<Language> detectedLanguages;
|
||||
|
||||
/// The attribute scores for this comment.
|
||||
final List<AttributeScore> attributeScores;
|
||||
|
||||
const AnalyzeCommentResponse({
|
||||
required this.languages,
|
||||
required this.detectedLanguages,
|
||||
required this.attributeScores,
|
||||
});
|
||||
|
||||
static AnalyzeCommentResponse parseAnalyzeCommentResponse(Map<String, Object?> res) {
|
||||
final {'languages': languages as List, 'detectedLanguages': detectedLanguages as List, 'attributeScores': rawAttributeScores as Map} = res;
|
||||
|
||||
final attributeScores = <AttributeScore>[];
|
||||
|
||||
for (final ra in rawAttributeScores.keys) {
|
||||
final attribute = RequestedAttribute.fromScreamingSnakeCase(ra);
|
||||
|
||||
final attributeScore = AttributeScore.parseAttributeScore(rawAttributeScores[ra] as Map<String, Object?>, attribute);
|
||||
|
||||
attributeScores.add(attributeScore);
|
||||
}
|
||||
|
||||
return AnalyzeCommentResponse(
|
||||
languages: languages.map((l) => Language.fromCode(l)).toSet(),
|
||||
detectedLanguages: detectedLanguages
|
||||
.map((dl) {
|
||||
try {
|
||||
return Language.fromCode(dl);
|
||||
} on ArgumentError {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whereType<Language>()
|
||||
.toSet(),
|
||||
attributeScores: attributeScores,
|
||||
);
|
||||
}
|
||||
}
|
72
lib/src/models/attribute_score.dart
Normal file
72
lib/src/models/attribute_score.dart
Normal file
@ -0,0 +1,72 @@
|
||||
import 'package:perspective_api/src/models/requested_attribute.dart';
|
||||
|
||||
class AttributeScore {
|
||||
/// The requested attribute.
|
||||
final RequestedAttribute attribute;
|
||||
|
||||
/// The summary score for this attribute.
|
||||
final double summaryScore;
|
||||
|
||||
/// The span scores for this attribute.
|
||||
final List<SpanScore> spanScores;
|
||||
|
||||
const AttributeScore({
|
||||
required this.attribute,
|
||||
required this.summaryScore,
|
||||
required this.spanScores,
|
||||
});
|
||||
|
||||
static AttributeScore parseAttributeScore(Map<String, Object?> res, RequestedAttribute attribute) {
|
||||
final {
|
||||
'summaryScore': sum as Map,
|
||||
'spanScores': spanScores as List,
|
||||
} = res;
|
||||
|
||||
final summaryScore = sum['value'] as double;
|
||||
|
||||
return AttributeScore(
|
||||
attribute: attribute,
|
||||
summaryScore: summaryScore,
|
||||
spanScores: spanScores.map((ss) => SpanScore.parseSpanScore(ss)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SpanScore {
|
||||
/// The beginning of the span.
|
||||
final int begin;
|
||||
|
||||
/// The end of the span.
|
||||
final int end;
|
||||
|
||||
/// The score for this span.
|
||||
final double score;
|
||||
|
||||
// /// The type of the span.
|
||||
// final String type;
|
||||
|
||||
const SpanScore({
|
||||
required this.begin,
|
||||
required this.end,
|
||||
required this.score,
|
||||
// required this.type,
|
||||
});
|
||||
|
||||
static SpanScore parseSpanScore(Map<String, Object?> res) {
|
||||
final {
|
||||
'begin': begin as int,
|
||||
'end': end as int,
|
||||
'score': sc as Map,
|
||||
// 'type': type as String,
|
||||
} = res;
|
||||
|
||||
final score = sc['value'] as double;
|
||||
|
||||
return SpanScore(
|
||||
begin: begin,
|
||||
end: end,
|
||||
score: score,
|
||||
// type: type,
|
||||
);
|
||||
}
|
||||
}
|
33
lib/src/models/language.dart
Normal file
33
lib/src/models/language.dart
Normal file
@ -0,0 +1,33 @@
|
||||
enum Language {
|
||||
english('en'),
|
||||
arabic('ar'),
|
||||
chinese('zh'),
|
||||
czech('cs'),
|
||||
dutch('nl'),
|
||||
french('fr'),
|
||||
german('de'),
|
||||
italian('it'),
|
||||
hindi('hi'),
|
||||
hinglish('hi-Latn'),
|
||||
indonesian('id'),
|
||||
japanese('ja'),
|
||||
korean('ko'),
|
||||
polish('pl'),
|
||||
portuguese('pt'),
|
||||
russian('ru'),
|
||||
spanish('es'),
|
||||
swedish('sv');
|
||||
|
||||
final String code;
|
||||
|
||||
const Language(this.code);
|
||||
|
||||
static Language fromCode(String code) => Language.values.firstWhere(
|
||||
(e) => e.code == code,
|
||||
orElse: () => throw ArgumentError.value(
|
||||
code,
|
||||
'code',
|
||||
'Invalid language code.',
|
||||
),
|
||||
);
|
||||
}
|
88
lib/src/models/requested_attribute.dart
Normal file
88
lib/src/models/requested_attribute.dart
Normal file
@ -0,0 +1,88 @@
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
enum RequestedAttribute {
|
||||
toxicity,
|
||||
severeToxicity,
|
||||
identityAttack,
|
||||
insult,
|
||||
profanity,
|
||||
threat,
|
||||
|
||||
// Experimental attributes.
|
||||
@experimental
|
||||
toxicityExperimental,
|
||||
@experimental
|
||||
severeToxicityExperimental,
|
||||
@experimental
|
||||
identityAttackExperimental,
|
||||
@experimental
|
||||
insultExperimental,
|
||||
@experimental
|
||||
profanityExperimental,
|
||||
@experimental
|
||||
threatExperimental,
|
||||
@experimental
|
||||
sexuallyExplicit,
|
||||
@experimental
|
||||
flirtation,
|
||||
|
||||
// N-Y-T attributes.
|
||||
attackOnAuthor,
|
||||
attackOnCommenter,
|
||||
incoherent,
|
||||
inflammatory,
|
||||
likelyToReject,
|
||||
obscene,
|
||||
spam,
|
||||
unsubstantial;
|
||||
|
||||
String toScreamingSnakeCase() {
|
||||
final split = name.split('');
|
||||
StringBuffer res = StringBuffer();
|
||||
|
||||
for (final char in split) {
|
||||
if (char != char.toLowerCase() && res.isNotEmpty) {
|
||||
res.write('_');
|
||||
res.write(char);
|
||||
} else {
|
||||
res.write(char);
|
||||
}
|
||||
}
|
||||
|
||||
return res.toString().toUpperCase();
|
||||
}
|
||||
|
||||
static RequestedAttribute fromScreamingSnakeCase(String snakeCase) {
|
||||
final split = snakeCase.split('_');
|
||||
StringBuffer res = StringBuffer();
|
||||
|
||||
for (final (i, word) in split.map((e) => e.toLowerCase()).indexed) {
|
||||
if (i == 0) {
|
||||
res.write(word);
|
||||
} else {
|
||||
res.write(word[0].toUpperCase());
|
||||
res.write(word.substring(1));
|
||||
}
|
||||
}
|
||||
|
||||
return RequestedAttribute.values.firstWhere(
|
||||
(e) => e.name == res.toString(),
|
||||
orElse: () => throw ArgumentError.value(
|
||||
snakeCase,
|
||||
'snakeCase',
|
||||
'Invalid attribute name.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static const Set<RequestedAttribute> restrictedLanguages = {
|
||||
attackOnAuthor,
|
||||
attackOnCommenter,
|
||||
incoherent,
|
||||
inflammatory,
|
||||
likelyToReject,
|
||||
obscene,
|
||||
spam,
|
||||
unsubstantial,
|
||||
};
|
||||
}
|
64
lib/src/perspective_api.dart
Normal file
64
lib/src/perspective_api.dart
Normal file
@ -0,0 +1,64 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/retry.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:perspective_api/src/http/request.dart';
|
||||
|
||||
import 'models/analyze_comment_response.dart';
|
||||
import 'models/language.dart';
|
||||
import 'models/requested_attribute.dart';
|
||||
|
||||
DateTime? _lastRequestTime;
|
||||
|
||||
final class PerspectiveApi {
|
||||
final String _apiKey;
|
||||
|
||||
final _client = RetryClient(Client(), when: (res) {
|
||||
return _lastRequestTime == null ||
|
||||
_lastRequestTime!.isAfter(
|
||||
DateTime.now().subtract(
|
||||
const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
PerspectiveApi({
|
||||
/// The API key to use for requests.
|
||||
required String apiKey,
|
||||
}) : _apiKey = apiKey;
|
||||
|
||||
/// Analyze a comment.
|
||||
Future<AnalyzeCommentResponse> analyzeComment(
|
||||
String text, {
|
||||
required Set<RequestedAttribute> requestedAttributes,
|
||||
required Set<Language> languages,
|
||||
}) async {
|
||||
final body = {
|
||||
'comment': {'text': text},
|
||||
'requestedAttributes': {
|
||||
for (final ra in requestedAttributes)
|
||||
ra.toScreamingSnakeCase(): {},
|
||||
},
|
||||
'languages': languages.map((l) => l.code).toList(),
|
||||
};
|
||||
|
||||
final request = BasicRequest(
|
||||
'comments:analyze',
|
||||
queryParameters: {
|
||||
'key': _apiKey,
|
||||
},
|
||||
body: json.encode(body),
|
||||
method: 'POST',
|
||||
);
|
||||
|
||||
final res = await _client.send(request.prepare());
|
||||
|
||||
_lastRequestTime = DateTime.now();
|
||||
|
||||
final resBody = (await res.stream.bytesToString());
|
||||
|
||||
final jsonBody = json.decode(resBody);
|
||||
|
||||
return AnalyzeCommentResponse.parseAnalyzeCommentResponse(jsonBody);
|
||||
}
|
||||
}
|
17
pubspec.yaml
Normal file
17
pubspec.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
name: perspective_api
|
||||
description: A starting point for Dart libraries or applications.
|
||||
version: 1.0.0
|
||||
# repository: https://github.com/my_org/my_repo
|
||||
|
||||
environment:
|
||||
sdk: ^3.0.5
|
||||
|
||||
# Add regular dependencies here.
|
||||
dependencies:
|
||||
http: ^1.1.0
|
||||
meta: ^1.10.0
|
||||
# path: ^1.8.0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.0.0
|
||||
test: ^1.21.0
|
0
test/perspective_api_test.dart
Normal file
0
test/perspective_api_test.dart
Normal file
Loading…
x
Reference in New Issue
Block a user